Repository navigation
Another mir-opt-level=2 miscompilation #67862
Copy link
Copy link
Closed
Labels
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlC-bugCategory: This is a bug.Category: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler 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.This issue requires a nightly compiler in some way. When possible, use a F-* label instead.
Description
On the last Nightly:
rustc 1.42.0-nightly (c5840f9d2 2020-01-03)
binary: rustc
commit-hash: c5840f9d252c2f5cc16698dbf385a29c5de3ca07
commit-date: 2020-01-03
host: x86_64-pc-windows-gnu
release: 1.42.0-nightly
LLVM version: 9.0
rustc -Z mir-opt-level=1 test.rs => OK
rustc -Z mir-opt-level=2 test.rs => Error
fn e220() -> String {
fn get_displacement(mut distance: i64, generation: i32) -> [i64; 4] {
if generation == 0 {
return match distance {
0 => [0, 1, 0, 0],
1 => [0, 1, 0, 1],
_ => panic!("Travel past end")
}
}
let half_len = 1i64 << (generation - 1);
let second_half = distance > half_len;
if second_half {
distance = half_len + half_len - distance;
}
let mut ret = get_displacement(distance, generation - 1);
let (ex, ey) = (ret[0], ret[1]);
ret[0] = ex + ey;
ret[1] = ey - ex;
if second_half {
let (px, py) = (ret[2], ret[3]);
ret[2] = ex + ey - py;
ret[3] = ey - ex + px;
}
ret
}
let res = get_displacement(1_000_000_000_000, 50);
format!("{},{}", res[2], res[3])
}
fn main() {
assert_eq!(e220(), "139776,963904");
}
Metadata
Metadata
Assignees
Labels
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlC-bugCategory: This is a bug.Category: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler 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.This issue requires a nightly compiler in some way. When possible, use a F-* label instead.
Activity
bjorn3 commentedon Jan 4, 2020
Reduced to:
bjorn3 commentedon Jan 4, 2020
Mir of
e220with-Zmir-opt-level=1:Mir of
e220with-Zmir-opt-level=2:StorageLive(_4);andStorageDead(_4);got removed while_4is still assigned and used. Also the fields of_2now getconst Scalar(AllocId(15).0x0)andconst Scalar(AllocId(15).0x8)assigned.Aaron1011 commentedon Jan 8, 2020
The are a couple of things going on here:
The immediate cause is the fact that we don't try to propagate operands in
TerminatorKind::Call. This means that_1 = const e220::get_displacement() -> bb1;never gets evaluated, leaving_1uninitialized.Normally, this would not be a problem - const-prop explicitly bails out when trying to access an uninitialized local, so failing to evaluate
_1 = const e220::get_displacement()should just mean that we don't const-propagate anything that relies on_1.Unfortunately, it's possible for an uninitialized local to be incorrectly marked as initialized. Inside
force_allocation, we unconditionally mark a local asLocalValue::Live, even if it was previously uninitialied. When processing anRvalue::Ref, we callforce_allocationon the source, which will incorrectly mark an uninitialized source as initialized.While implementing const-propagation for
TeminatorKind::Callwould fix this specific issue, theforce_allocationissue means that we risk introducing undefined behavior into valid programs whenever const-propagation doesn't evaluate something that would initialize a local.I see a few ways different of solving this:
LocalValue::UninitializedtoLocalValue::Uninitialized(Option<MemPlace>). Whenforce_allocationis called on an uninitialized local, we keep the local as aLocalValue::Uninitialized, but would storeSome(mplace)in it. This would ensure thatforce_allocationhas no user-visible effect - uninitialized locals will remain uninitialized until actually written to.force_allocationaware of whether not or not it is being used for 'reading' or 'writing'. When 'reading', we callaccess_localbefore overwriting a local withLocalValue::Live, allowing the machine to assert that it is valid. When 'writing', we skip callingaccess_local, allowingforce_allocationto be used when initializing a previously uninitialized local (which does not require that the target local be initialized).cc @RalfJung
RalfJung commentedon Jan 8, 2020
Without having digested all the details yet, my first thought is: this is caused by const-prop trying to shoehorn the Miri engine (built for evaluation of whole programs) into an engine for partial evaluation.
The
LocalValue::Uninitializedwas never meant for tracking things like "unkown value", it is a hack introduced for handling unsized locals as I didn't find a better way to implement them. It's meaning is "a local whose size we don't know yet".force_allocationcan determine the size, and thus complete allocation of the local.In terms of the Rust Abstract Machine (which the Miri engine models), I don't know what an "uninitialized local" is -- but a local that's marked
Livebut whose memory is entirely uninitialized should certainly qualify. Thus I would say thatforce_allocationcorrectly preserves the uninitialized nature of the local.The issue is that const-prop is taking the difference between
LocalValue::UninitializedandLocalValue::Live, which is an implementation detail of the engine (but doesn't correspond to anything real, it's basically just an optimization), and tries to assign meaning to it. No wonder this went wrong.What exactly is it that const-prop needs?
RalfJung commentedon Jan 8, 2020
Cc @oli-obk @wesleywiser
oli-obk commentedon Jan 8, 2020
Zero cost uninitialized locals is the base feature we need. Essentially if we decide we don't need a local, reading its memory should yield an uninit value and its memory should not require any heap allocations.
I get the shoehorning, we so are doing this a lot in const prop, mainly to keep the engine modifications minimal. I guess we could add some more machine hooks to get rid of hacks.
RalfJung commentedon Jan 8, 2020
Even with
force_allocation, reading it will yield an uninit value. So not requiring heap allocations is a correctness condition? That's what @Aaron1011's analysis says, basically.oli-obk commentedon Jan 8, 2020
Well... it shouldn't be. The
::Livecheck is actually just a check ensuring we don't ICE for unsized values.This is not worded correctly. The uninitialized local is marked as live its memory is still uninitialized. But that doesn't change the fact that the analysis is spot on. We take references to locals that may never get a value propagated into.
The early bail out in
rust/src/librustc_mir/transform/const_prop.rs
Line 222 in 87540bd
Yea, so basically whenever we don't fully propagate into a local, but later take a reference to that local, we end up with a const pointer to an uninitialized const allocation.
Basically unless the local's memory is valid (as per the validation rules for statics), we should not permit taking references to it.
Aaron1011 commentedon Jan 8, 2020
Doesn't this also affect normal uses of an un-propagated local? We'll end up copying data from an uninitialized allocation, which will result in the same issue.
oli-obk commentedon Jan 8, 2020
Yes, any local that we don't fully propagate into but that is referred to by a local that we marked as "could be propagated".
RalfJung commentedon Jan 9, 2020
I'd say it's less about validity and more about whether we actually did all the computation for this local. That's not the same thing.
Basically, const-prop needs a state representing "unknown". This is distinct from "(known to be) uninitialized". And that's not surprising at all, static analyses and optimizations almost always have such a state. This is a first step towards supporting abstract interpretation in Miri.
oli-obk commentedon Jan 9, 2020
We inverted this by treating any initialized values as "!unknown". The issue here is that taking a reference to a value works even if the value is not fully initialized. We have no concept of fully initialized, and in case the local is of union type, we can't rely on the uninitialized state to signal "maybe unknown".
So yea, the validity check won't work here either. @wesleywiser and I talked about using dataflow for const prop, so we'd mark locals only as known when they have been successfully propagated into instead of relying on the initialization state. Until we do this we should probably just turn off propagating reference taking.
wesleywiser commentedon Jan 9, 2020
I agree disabling ref propagation makes sense. We have quite a few bugs that I believe would be resolved by that. In addition, I believe there's only 1 or 2 MIR tests that would be affected.
Going forward, perhaps we should try to find some way to get more coverage of mir-opt=2?
RalfJung commentedon Jan 10, 2020
The other problem is (as you hinted at) that things, at least in theory, might be "partially known", such as in
where
_5is, say, a pair: now the first field is known but the second is not.RalfJung commentedon Jan 10, 2020
A general worry about this is how our const-prop looks not at all like the usual "compiler construction literature const-prop": the usual thing is to do a fixed-point (static) analysis on a domain including all possible values as well as "unknown". That could also handle loops, which I suppose you currently have to carefully avoid. It is certainly possible to implement constant propagation differently, but that means none of the standard intuition about it applies.
Turn off const propagation of ref taking
Auto merge of #68170 - wesleywiser:stop_const_prop_ref_taking, r=oli-obk