You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Casting infinity to an intiger sometimes results in a 0 value(with optimizations enabled) #690
changed the title [-]Compiler bug found using rustlantis.[/-][+]Casting infinity to an intiger sometimes results in a 0 value(with optimizations enabled)[/+]on May 30, 2025
In C this is UB. LLVM used to have it be UB too for a long time, requiring rustc to check for overflow before doing the actual cast, but now there is an intrinsic which saturates on overflow. I guess GCC has the same UB.
I would have to check.
The more fundamental issue here is wrapping vs saturating casts. I believe
that on x86_64 C casts wrap around, while in Rust they saturate.
It seems to me like they are saturating(in `cg_gcc`), but in a subtly wrong way.
If they were wrapping that should cause almost every file generated by
rustlantis to fail.
Looking at the generated assembly, I might have a rough idea about the case
of the bug.
Here, it looks like if the float value > 0, then we cast it to a float, and then check if it is larger than the max int value after the fact(swaping the value out in that case).
Since such a cast is UB if the floating point is out of range, GCC is able to "see" the loaded value can't possibly be larger than max(since that is UB), and removes the branch.
So, it seems like all of the muck at the begining is needed to get GCC to "see" that the branch is impossible.
I do not know if this is the problem here, but that kind of ordering issues can happen because GCC uses an AST-based IR while LLVM uses an instruction-based IR.
Which means that sometimes, we need to explicitly tell GCC to insert some instruction right now (for instance, load the value in a variable right now, instead of just forwarding the value which could be dereferenced in a another basic block).
// 1. Cast val to an integer with fpto[su]i. This may result in undef.
At least the comment here suggests the order of operations(cast then check) is simply wrong.
We can work around this by swapping the order of operations around.
For unsigned casts, maybe could do something clever with clamps or min/max, e.g. (int) min(float_max, max(val, 0,0)) which should result in the correct value, even for infinites and NaN.
It looks like, in this case, GCC only applied the optimizations when computing the floating-point value for the cast, but not when it got reinterpreted as bits(or passed to be displayed).
In other words, the value of that float is indeterminate(is this the right term?): each time we look at it, it changes.
I have honestly no idea.
I don't know if the problem really is that 2 different values are observed: it seems related to the float comparison.
For instance, printing signbit(res) will show the same value for both debug and release builds.
And putting the comparison in a noinline function:
int __attribute__ ((noinline)) is_positive(floatf) {
returnf>0.0;
}
@FractalFir As far as I can tell this is just a plain GCC bug (somewhere in VRP presumably).
The result of the float calculation here is required to be +inf, and the result of the comparison must be true. All the funky float non-determinism stuff is related to NaN values (which are not involved here) and requires inspecting the bitwise representation of the float (float comparisons are not affected by NaN signs).
Activity
FractalFir commentedon May 30, 2025
After further reduction, all of this is just a very long way of obtaining infinity.
With optimizations enabled, GCC will optimize the
inf as u64cast to return 0, instead of u64::MAX.[-]Compiler bug found using rustlantis.[/-][+]Casting infinity to an intiger sometimes results in a 0 value(with optimizations enabled)[/+]bjorn3 commentedon May 30, 2025
In C this is UB. LLVM used to have it be UB too for a long time, requiring rustc to check for overflow before doing the actual cast, but now there is an intrinsic which saturates on overflow. I guess GCC has the same UB.
FractalFir commentedon May 30, 2025
Huh, I did not expect this to remain uncaught for so long.
cg_clrhas a workaround for this, but it looks likecg_gccdoes not.https://github.com/rust-lang/rustc_codegen_gcc/blob/master/src%2Fbuilder.rs#L1212-L1216
I guess we will have to implement a workaround here too. Thankfully, it is a fairly simple thing to do.
There is one thing that concerns me, tough. The x86_64 float to int casts are wrapping, right?
So, with the default settings, this should have tripped way more tests.
This suggests to me that there is a workaround in play already, but it is just broken.
@antoyo does
cg_gccset any flags that could influence this? E.g. do you switch float casts to saturating somehow?antoyo commentedon May 30, 2025
I do not think so. We set some flags like
-fwrapv, but I don't think this would influence this.Any idea of tests that should fail because of this bug?
FractalFir commentedon May 30, 2025
FractalFir commentedon May 30, 2025
Analyzing the GIMPLE we generate is a bit tough - duplicate block names make it hard to see what is happening.
So, it looks like the "saturation check" is present on the GIMPLE level, the problem just is... it happens after the cast.
Here, it looks like if the float value > 0, then we cast it to a float, and then check if it is larger than the max int value after the fact(swaping the value out in that case).
Since such a cast is UB if the floating point is out of range, GCC is able to "see" the loaded value can't possibly be larger than max(since that is UB), and removes the branch.
So, it seems like all of the muck at the begining is needed to get GCC to "see" that the branch is impossible.
FractalFir commentedon May 30, 2025
I still don't get why the casts are sometimes saturating in the first place.
A piece of code like this works(is saturating):
But it should not, since a bit of equivalent C, compiled with GCC, does not(it wraps around).
antoyo commentedon May 30, 2025
I do not know if this is the problem here, but that kind of ordering issues can happen because GCC uses an AST-based IR while LLVM uses an instruction-based IR.
Which means that sometimes, we need to explicitly tell GCC to insert some instruction right now (for instance, load the value in a variable right now, instead of just forwarding the value which could be dereferenced in a another basic block).
FractalFir commentedon May 30, 2025
I can't find where that branch is inserted. If it is not inserted by cg_gcc(I am not sure about that), then maybe it is inserted by libgccjit?
antoyo commentedon May 30, 2025
It could be inserted in
cg_ssaas well.I would be surprised if
libgccjitinserted that.FractalFir commentedon May 30, 2025
Never mind, it is in
cg_gcc- I just could not find it. And, I immediately see the issue:rustc_codegen_gcc/src/builder.rs
Line 1841 in 706905b
At least the comment here suggests the order of operations(cast then check) is simply wrong.
We can work around this by swapping the order of operations around.
For unsigned casts, maybe could do something clever with clamps or min/max, e.g.
(int) min(float_max, max(val, 0,0))which should result in the correct value, even for infinites and NaN.FractalFir commentedon May 31, 2025
So, I reimplemented the cast in a way that I think should be fully sound:
But it is still broken with optimizations. Can you see anything obviously wrong with this cast?
EDIT:
@antoyo
Could you please reopen the issue?
I got fat fingers, and Github's got some short-cuts I don't know about :(. I can't reopen it myself, sadly.
FractalFir commentedon May 31, 2025
It seems like I can replicate the issue in C. I think the following code ought to be UB free, right?
antoyo commentedon May 31, 2025
Could you please push your branch somewhere and tell me exactly which reproducer produces that GIMPLE IR?
FractalFir commentedon May 31, 2025
The branch is here: https://github.com/FractalFir/rustc_codegen_gcc/tree/master
Both the snippet of gimple, and the GIMPLE converted to C come from this small Rust reproducer.
The
castfunction exists only because I wanted to check exactly what gimple I generated for casts.FractalFir commentedon May 31, 2025
I further minimzed the C example, removing the cast alltogether. This function returns true in -O1, but false in -O2.
Since it only contains floating-point math(which is UB-free?), it should behave identically in both cases.
EDIT: I think I know what is going on.
Here, we get screwed over by the sign of infinity.
If you run this Rust code:
You'll see that casting inf and -inf to a u64 results in different values.
Here, GCC's floating-point optimizations change a positive infinity to a negative one. I think this is OK in C(not sure, tough).
I don't know if this is a miscompilation according to Rust(I think so).
antoyo commentedon May 31, 2025
There's been a lot of discussion about the float semantics in Rust. You might be able to find the answer to that question in one of those links:
float_semanticsRFC 3514 rust#128288NaNs rust#73328FractalFir commentedon May 31, 2025
When I looked at the bits of the float(or the float itself), they were identical.
It looks like, in this case, GCC only applied the optimizations when computing the floating-point value for the cast, but not when it got reinterpreted as bits(or passed to be displayed).
In other words, the value of that float is indeterminate(is this the right term?): each time we look at it, it changes.
FractalFir commentedon May 31, 2025
This has the potential to royally screw us over. Consider a cast like this:
Suppose GCC optimizes the value in the 1st check to -infinity, and keeps the rest as infinity.
Now, our code looks like this:
Since negative infinity is less than
ULONG_MAX, we fall trough to this code:Positive ininfity is more than 0, so we fall trough here:
And now, our code has UB it did not have before. On some systems, this could cause a crash.
FractalFir commentedon May 31, 2025
Manifesting this issue in C is quite easy. Here, we can see that:
resis positive infinity, with the bit pattern 0x7f800000(the sign bit is not set)resis, at the same time, smaller than 0.@antoyo do you know if this behaviour(the same variable has 2 different values when accessed) is permitted by the C standard?
antoyo commentedon May 31, 2025
I have honestly no idea.
I don't know if the problem really is that 2 different values are observed: it seems related to the float comparison.
For instance, printing
signbit(res)will show the same value for both debug and release builds.And putting the comparison in a
noinlinefunction:gives the correct result.
nikic commentedon Jun 1, 2025
@FractalFir As far as I can tell this is just a plain GCC bug (somewhere in VRP presumably).
The result of the float calculation here is required to be
+inf, and the result of the comparison must be true. All the funky float non-determinism stuff is related to NaN values (which are not involved here) and requires inspecting the bitwise representation of the float (float comparisons are not affected by NaN signs).nikic commentedon Jun 1, 2025
https://gcc.godbolt.org/z/4j66dWz6s has vrp1 printing something like this:
If I'm reading this right, it seems to think that
bcan only be NAN, which is ... not right.