Skip to content

Zpolonius=next soundness bug: defining use of an opaque type discards the first one's region #160669

Closed

Description

@carlini

I tried this code:

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

fn foo<'a>(s: &'a String, flag: bool) -> impl Display + use<'a> {
    if flag {
        let local = String::from("dangling");
        return &local;
    }
    s
}

fn main() {
    let s = String::from("ok");
    let r = foo(&s, true);
    println!("{}", r); // reads freed heap + stack memory
}

compiled with -Zpolonius=next.

I expected to see this happen: the program is rejected

Instead, this happened: -Zpolonius=next accepts it, and the program segfaults. Miri (with RUSTFLAGS=-Zpolonius=next) reports UB.

Meta

rustc --version --verbose:

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

LLM-written explanation of what it thinks is happening

Summary: the body has two defining uses of the one opaque key —
return &local (first in MIR order) and s (last). MIR typeck's opaque
storage keeps one hidden type per key: the second registration replaces the
first, and the goals equating old == new are filed at
Locations::Single(<second use's point>). Only the last hidden type is tied
to the signature's captured slot at Locations::All. Under =next's
location-sensitive constraint graph, the first hidden type's region is a
fresh generalizer variable that is live nowhere, so the loan of local
stops dead at the first return point, drop(local) sees no live loan, and
the dangling &String is returned. Under NLL the same constraints are
location-insensitive, the whole chain collapses into one SCC, and nothing is
lost — which is exactly why NLL rejects and =next accepts.

What goes wrong

  • compiler/rustc_borrowck/src/type_check/relate_tys.rs:161 —
    NllTypeRelating::relate_opaques ends with
    self.register_goals(infcx.handle_opaque_type(a, b, ...)), and
    register_goals (same file, :568-592) files those goals as an
    InstantiateOpaqueType type-op at self.locations (:573), i.e.
    Locations::Single(<the current use point>).
  • The infcx's opaque storage holds exactly one hidden type per key; a second
    registration mem::replaces the first and hands back goals equating
    prev == new. Those land at the second use's single point.
  • region_infer/opaque_types/mod.rs::apply_definition_site_hidden_types
    ties only the entry still in storage — the last hidden type — to the
    signature's captured slot at Locations::All.
  • compiler/rustc_borrowck/src/polonius/constraints.rs:74-88 —
    Locations::All becomes a logical edge materialized at every point;
    Locations::Single is a physical edge at that one point only. A loan only
    walks the CFG through a region that is universal or explicitly live at the
    next point; the first hidden type's region is neither.

So for every earlier registration the only route from its region to the
captured slot is a chain whose first link exists at one single point, and the
loan of local cannot traverse it. The Locations::All doc comment at
type_check/mod.rs:326-362 describes this exact failure mode for the return
slot; handle_opaque_type's goals are the same kind of fact and are missing
the same treatment.

Note the bug is order-dependent (MIR traversal order decides which hidden
type is discarded); opaques with a single defining use are unaffected.

Controls (all on the unfixed compiler):

variant result
as above, default borrowck rejected E0597
as above, -Zpolonius (legacy) rejected E0597
delete the early return (single defining use s) accepted everywhere, sound
return s in both arms accepted everywhere, sound

Fix direction: file the goals returned by handle_opaque_type at
Locations::All instead of Locations::Single, matching the treatment the
captured-slot constraints already get in
apply_definition_site_hidden_types.

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
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    on Aug 7, 2026
  3. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Aug 7, 2026
  4. added
    P-highHigh priority
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Aug 7, 2026
  5. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Aug 7, 2026
  6. added
    NLL-poloniusIssues related for using Polonius in the borrow checker
    requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    A-impl-traitArea: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch.
    on Aug 7, 2026
  7. maxdexh commented on Aug 7, 2026

    @maxdexh
    Member

    Slightly simplified (playground)

    use std::fmt::Debug;
    
    fn foo() -> impl Debug {
        if true {
            let local = String::from("dangling");
            return &local;
        }
        Box::leak(Box::new(String::new()))
    }
    
    fn main() {
        let r = foo();
        println!("{:?}", r); // reads dangling reference
    }

    Somehow this crashes with a stack overflow :D

  8. maxdexh commented on Aug 7, 2026

    @maxdexh
    Member

    Also, it errors when building with -Znext-solver. Does that turn off polonius or does this bug actually only happen when using old solver + polonius?

  9. asquared31415 commented on Aug 7, 2026

    @asquared31415
    Contributor

    (it's not actually a stack overflow, that's the SIGSEGV handler seeing an address that happens to look like it might have been from the stack, and assuming that it was a stack overflow. the problem is still just a read from an invalid pointer.)

  10. lqd commented on Aug 17, 2026

    @lqd
    Member

    Also, it errors when building with -Znext-solver. Does that turn off polonius or does this bug actually only happen when using old solver + polonius?

    The latter.

    As said above, the examples don't build when using the next-solver at all regardless of borrow-checking implementations: this looks like a weakness with the way the old solver handles opaques. The next solver will also be enabled on nightly imminently, so I'm not sure we should do anything here.

  11. added a commit that references this issue on Aug 18, 2026
    cda599c
  12. added a commit that references this issue on Aug 19, 2026
    67b7b27
  13. added a commit that references this issue on Aug 20, 2026
    d472d9a
  14. added a commit that references this issue on Aug 20, 2026
    e380a1f
  15. added 2 commits that reference this issue on Aug 24, 2026
    6c388bb
    17cc0af
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-impl-traitArea: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch.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