Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix undefined behaviour of infinity() #13884

Open
wants to merge 2 commits into
base: 2.4
Choose a base branch
from
Open

Conversation

daschuer
Copy link
Member

This fixes the undefined behviour of of std::numeric_limits<double>::infinity()) in -ffastmath (-ffinite-math-only) code. By moving it to fclassify.cpp which is compiled without this flags leading to defined behavior. It makes sure that the IEEE infinity is returned which is 0x7FF0000000000000.

Clang 18 warns about this as use of infinity is undefined behavior due to the currently enabled floating-point options. This warning may indicate a real issue in case of aggressive optimization.

This solution not only mutes this warning as -Wnan-infinity-disabled. We have argued that we shall not use infinity at all, but we can't entirely prevent it because all UB does not mean infinity does not happen especially when we interact with non -ffastmath libraries.

Fixes: #13780

The discussion around using inf() as invalid value in this is orthogonal. We have test in place that verify that it is working for now. I consider a refactoring of it as unnecessary regression risk, but will not block an attempt. Just let's merge this as a fix for UB.

Copy link
Member

@Swiftb0y Swiftb0y left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good solution (nor does it fix undefined behavior, it just silences the warnings) but I couldn't find any good resources on what is UB and what isn't in -ffinite-math-only to make a well founded argument against it here.

The only suggestion I would like to make here is that instead of introducing util_double_infinity and making it available in source code and tests, we should consider simply removing -ffast-math from the tests so we can use the <limits> facilities as originally assumed.

@daschuer
Copy link
Member Author

daschuer commented Nov 14, 2024

Let me explain my view a bit more:
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-ffinite-math-only

Allow optimizations for floating-point arithmetic that assume that arguments and results are not NaNs or +-Infs.

For my understanding that means, that all results from floating-point arithmetic where NaNs or +-Infs or ifs are involved are undefined behavior.

The compiler warns that using std::numeric_limits<double>::infinity()) is also undefined behavior. The movement to non -ffastmath code fixes this issue effectively without any doubts.

Copying float values around is not floating-point arithmetic, in addition there is no gain for optimization. So we can be sure that the result is well defined even in ffastMath code. It is just copying 0x7FF0000000000000 like any other data type.

The unittest helps to confirm that. If we follow you suggestion to not set the -ffastMath flag, the unittest doe no longer represents the Mixxx code where the fclassify functions are used in that way.

Changing the test falgs only affets the tests, where we see immediately any issues in our CI. So I would not worry that much.

Considering, this we may add a reinterprete cast test, checking the IEEE infinity raw value.

@Swiftb0y
Copy link
Member

Swiftb0y commented Nov 15, 2024

Right, I see that. My primary problem is that adding util_*_infinity to produce Inf in finite-math-only code makes it too easy to produce these values in contexts where they're not expected and thus produce UB. I agree that copying these values is probably safe, but its too easy to accidentally do something unsafe if you can synthesize that value anywhere. util_*_infinity is a prime example of what is commonly referred to as a "footgun" because its very hard to not do something stupid with it.

IMO the proper solution would be to create a separate opaque type, compiled without finite-math-only and if you want to use infinity, you can only do it through that type. Conversion from that type to double would then do that check. I would offer to create this type if you agree that this is a better solution.

@daschuer
Copy link
Member Author

Right, I see that. My primary problem is that adding util_*_infinity to produce Inf in finite-math-only code makes it too easy to produce these values in contexts where they're not expected and thus produce UB.

I can confirm you concerns. This shall however not block this PR, because it just fixes an UB risk that already exists. Without the newly introduced clang-18 warning it is even more easy to do it but I can not really imagine how anyone wants to do math with infinity.
The only case I can imagine is:

    double a; 
    double b;
    double c = a / b;
   if (c == infinity()) {
   ...
   }

However in case b is 0, the value of c is already UB before the comparison.

@Swiftb0y
Copy link
Member

I can not really imagine how anyone wants to do math with infinity.

Right, it doesn't make sense when looking at it from a microscale, but once infinity passes function boundaries (which it does) its not as clear anymore. In the context of finite-math-only code, infinity needs to be treated just as careful as nullptr when doing operations on pointers. Only this time, checking for it is even more expensive...

@Swiftb0y
Copy link
Member

in practice instead of messing with this hack, we can simply replace the usage of infinity as a sentinel value in FramePos with a different (safe) sentinel value and we're good. mixxx::Duration is basically deprecated and from what I can tell, there are no other uses anyway...

@daschuer
Copy link
Member Author

We are here here in a stable branch. The work can be done in the main branch if you wish.

However I cannot follow your reasoning. Because division by zero is always there in ffastmath it is UB and without infinity. This patch does not relate to your concerns.

In the context of finite-math-only code, infinity needs to be treated just as careful as nullptr when doing operations on pointers. Only this time, checking for it is even more expensive...

That is not true. Independent from finite-math-only we must not do calculations that result in infinity, because it is a dead end. It is reasonable to check devisors for zero and similar before doing the calculation and go the safe branch.

The difference is that the compiler does not check for it in such code. It assumes inf does not exist and the result is whatever the CPU does with it.

You see. There is no issue here and all expectations are verified by unittest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants