Repository navigation
Segfault or wrong values from const FOO: u32 = 0 - 1; println!("{}", FOO); #50814
Description
Activity
The constant evaluation error is being flagged, but only as a warning, so the code is still executed.
cc @oli-obk
- addedI-crashIssue: The compiler crashes (SIGSEGV, SIGABRT, etc). Use I-ICE instead when the compiler panics.Issue: The compiler crashes (SIGSEGV, SIGABRT, etc). Use I-ICE instead when the compiler panics.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.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.Performance or correctness regression from one stable version to another.C-bugCategory: This is a bug.Category: This is a bug.A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)Area: Constant evaluation, covers all const contexts (static, const fn, ...)
on May 16, 2018 @eddyb the way promotion works we can't back out of promoting in case of const eval failure, can we?
The named const case can be fixed. But I'm not sure what do do on the promotion case
@oli-obk Ah, this is where we disagree - overflow is not an evaluation error.
That is, because this is compiled in release mode, there's noAssertMIR terminator, which means that wrapping should happen.But there is a more general issue, where if a
const fnis called, and evaluation fails in it (for whatever reason, e.g. wrong unsafe code), a promoted call to it doesn't inject a runtime panic.
So I think we have to add that panic to the runtime codegen, and keep compile-time warnings.Ok so this is super easy to fix. Just remove the check in https://github.com/rust-lang/rust/blob/master/src/librustc_mir/interpret/eval_context.rs#L533
The const fn part is "fine" right now, because users can't write any const fns and the const fns that libstd offers are safe to use.
@oli-obk I would not rely on assumptions about libstd, IMO, it's too fragile. We have code to emit an
Assertfailure, just need to reuse it for errors while evaluating a promoted.- addedI-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
on May 18, 2018 Ok. some irc discussion has shown that this is a horrible situation we're in. Summary:
Let's use a strawman union for demonstration purposes, but it's not necessary, there are other ways to fail the const evaluator, they just aren't short examples.
/// Helper for casting `&'static u32` into `usize` union Union { a: &'static u32, b: usize, }
unsafe { Union { a: &1 }.b == Union { a: &2 }.b }is perfectly sane at runtime (but not doable at compile-time right now. This is just a placeholder example. Take any piece of code that works at runtime but should be const evaluable but isn't at the moment.- This means that perfectly safe and working code that in the future could become promoted, would, after promotion, not be evaluable by miri. We can't report an error, because that would be a breaking change, we also can't turn this into a runtime panic, because that's also a breaking change. So we'd need to keep doing the original runtime code before promotion. We can't do that, because we need the result to be in static memory because promotion already promised the
'staticlifetime.
- This means that perfectly safe and working code that in the future could become promoted, would, after promotion, not be evaluable by miri. We can't report an error, because that would be a breaking change, we also can't turn this into a runtime panic, because that's also a breaking change. So we'd need to keep doing the original runtime code before promotion. We can't do that, because we need the result to be in static memory because promotion already promised the
- We can't make promotion depend on the const evaluation succeeding, because typeck needs to know whether promotion will actually succeed. At typeck time we don't know the generic arguments to non-monomorphized function.
- We could do a breaking change and make promoteds like regular statics, so you can't depend on the surrounding function's generics. That would allow typeck to simply evaluate the promoted during the creation of the promoted and skip promoting it in the case of errors. I hate suggesting this, but it seems like the sanest option.
- rustc 1.25 just hard errored at monomorphization time. We can reintroduce that error, but it'll mean we can never improve promotion to accept more code.
Here's a problematic case with depending on a generic function's args:
trait C { const BOO: usize; } trait Foo<T> { const BAR: usize; } struct A<T>(T); impl<T: C> Foo<T> for A<T> { const BAR: usize = [5, 6, 7][T::BOO]; } fn foo<T: C>() -> &'static usize { &<A<T> as Foo<T>>::BAR } impl C for () { const BOO: usize = 42; } impl C for u32 { const BOO: usize = 1; } fn main() { println!("{:x}", foo::<()>() as *const usize as usize); println!("{:x}", foo::<u32>() as *const usize as usize); println!("{:x}", foo::<()>()); println!("{:x}", foo::<u32>()); }
You cannot implement this with
static, just with promotion1 remaining item
Looks like I ran into a variant of this as well:
trait Unsigned { const MAX: u8; } struct U8(u8); impl Unsigned for U8 { const MAX: u8 = 0xff; } struct Sum<A,B>(A,B); impl<A: Unsigned, B: Unsigned> Unsigned for Sum<A,B> { const MAX: u8 = A::MAX + B::MAX; } fn foo<T>(_: T) -> &'static u8 { &Sum::<U8,U8>::MAX } fn main() { foo(0); }Unlike the OP's examples, this still fails on rustc 1.28.0-nightly (cb20f68 2018-05-21), and doesn't produce any compile time warnings or errors (it also fails on 1.26.0).
For future reference: we're discussing this issue on Discord with the conversation GUID: 5fd923d1-f8ea-4a3a-a85b-67dbe3557544 (you can search for it =)
Notes from that discussion are available in this gist
https://gist.github.com/nikomatsakis/eee212d7c4a49ae75e19765c1e06bf49
What would such "strict limitations" on const fns in promotion look like?
Also: Could we simply not allow any const fn's in promoted expressions? This would mean some loss of expressivity in promotion, but seems like a simpler mental model.
Reacted by Eduard-Mihai BurtescuI'm still trying to understand what the problem is here, so let me try to rephrase and please tell me if that makes any sense:
What happens with promotion is that the compiler identifies some expression to be a promotion candidate. Such expressions than are evaluated at compile time using miri's CTFE. If that succeeds, all is well and the
&...is turned into the address of the new static. However, if CTFE fails then we have to either (a) abort compilation, or (b) compile the&...into an abort/panic. Currently, something goes awfully wrong and the&...turns into "undef", which is just bad and leads to the SEGFAULT here.Either way, there is the problem that the check if something should be promoted has to be very conservative because if it claims something is promotable but, but then that code errors at CTFE time, we broke compilation for no good reason (as this code would have compiled just fine if we didn't decide to promote).
Does that summarize the problem?
I see two separate things to do/discuss here, though they are obviously related. One is whether we pick (a) or (b). (The current situation of emitting "undef" is clearly a bug, right?) I don't see any good reason to to (b) -- silently (or with a warning?) emit code that will definitely panic at run-time seems just bad. Also, @oli-obk mentioned that it's not even easy to do because
panicis a block terminator and the place where this code is emitted does not expect a block terminator. So, I think we should do (a).The notes @nikomatsakis linked above mention (a), but only as an alternative to "forbid unions in promotion", which I don't understand how that's an alternative. We still have to do something if miri errors, right?
Secondly, how can we avoid the problem of trying to promote something that's not promoteable? This ties into the discussions around CTFE soundess/correctness at #24111 (comment). @oli-obk was convinced that we should have a "const mode typesystem" that makes things like raw pointer comparisons or ptr-to-int-casts unsafe. Based on that, shouldn't it be enough to check that the thing to be promoted is entirely "const well-typed" ignoring unsafe blocks, i.e., everything in there would have to be called in safe const code. That would rule out union field access.
There is still some possible issue if a
const fnclaims to be safe but actually does things that make miri error. However, I think that's okay: Such aconst fnis wrong, it's as wrong as a safefnthat triggers UB for a valid input! If someone writesconst fn, and they use unsafe code in there, ten they are fully responsible for making sure this code will not ever fail in CTFE. If they don't follow this rule, they screw up their user -- such is the fate of unsafe code. I think it's fine to also do that in this case.#24111 (comment)The current situation of emitting "undef" is clearly a bug, right?
It was intended for errors from things like out-of-bounds indexing and division by zero, where the original
Assertterminator stays in the runtime code and gets tripped before theundefis observed. In retrospective, what I should've done is replace theundefwith allvm.trapcall.I have addressed the
unionissue in #51328- added a commit that references this issue
on Jun 6, 2018 There is a full fix in #51570, but it's overzealous and will error on completely fine code. The correct solution is outlined in #51570 (comment) but that requires some refactorings that @TheDarkula is currently working on. After their refactorings are done I'll update the PR to implement the fix properly.
Thanks to #52571 (which got backported to beta and hence will be in the next stable), at least similar bugs can no longer create UB. Instead, the program will abort. Not good but better.^^
That means this is no longer I-unsound.
closing at the actual soundness holes have been plugged (by codegening abort code instead, as @RalfJung described.)
A new issue will hopefully be opened somewhere soon to track the follow-on work to do better static checking in such cases rather than falling back on dynamic aborts.
I did a (short) writeup with a possible solution in rust-lang/const-eval#2
The following programs produce a binary that will either segfault or produce a nonsense value seemingly depending on whether optimizations are enabled or not.
On 1.25.0 they behave as expected (the first doesn't even compile) but 1.26.0 and newer are broken.