Repository navigation
Description
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_opaquesends with
self.register_goals(infcx.handle_opaque_type(a, b, ...)), and
register_goals(same file,:568-592) files those goals as an
InstantiateOpaqueTypetype-op atself.locations(:573), i.e.
Locations::Single(<the current use point>).- The infcx's opaque storage holds exactly one hidden type per key; a second
registrationmem::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 atLocations::All.compiler/rustc_borrowck/src/polonius/constraints.rs:74-88—
Locations::Allbecomes a logical edge materialized at every point;
Locations::Singleis 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
maxdexh commentedon Aug 7, 2026
Slightly simplified (playground)
Somehow this crashes with a stack overflow :D
maxdexh commentedon Aug 7, 2026
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?asquared31415 commentedon Aug 7, 2026
(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.)
lqd commentedon Aug 17, 2026
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.
Auto merge of #161151 - jackh726:issue-160669-subtyping-live, r=lqd
Auto merge of #161151 - jackh726:issue-160669-subtyping-live, r=lqd
Auto merge of #161151 - jackh726:issue-160669-subtyping-live, r=lqd
Auto merge of #161151 - jackh726:issue-160669-subtyping-live, r=lqd
Auto merge of #161151 - jackh726:issue-160669-subtyping-live, r=lqd
Auto merge of #161151 - jackh726:issue-160669-subtyping-live, r=lqd