Issue 185183
Summary [InstCombine] Fold of `minimumnum(x, fneg x)` to `fneg(fabs(x))` is incorrect for signaling NaNs
Labels new issue
Assignees
Reporter cardigan1008
    Here is a miscompilation case when reviewing https://github.com/llvm/llvm-project/pull/180529:

```llvm
define i32 @test_minimumnum_x_negx(float %x) {
  %negx = fneg float %x
  %min = call float @llvm.minimumnum.f32(float %x, float %negx)
  %r = bitcast float %min to i32
  %res = lshr i32 %r, 24
  ret i32 %res
}

declare float @llvm.minimumnum.f32(float, float)

define i32 @main(i32 %argc, ptr %argv) {
entry:
  %r = call i32 @test_minimumnum_x_negx(float bitcast (i32 2139095041 to float))
  ret i32 %r
}
```

With opt built on this patch, it's transformed into:

```llvm
define i32 @test_minimumnum_x_negx(float %x) {
  %1 = call float @llvm.fabs.f32(float %x)
  %min = fneg float %1
 %r = bitcast float %min to i32
  %res = lshr i32 %r, 24
  ret i32 %res
}

; Function Attrs: nocallback nocreateundeforpoison nofree nosync nounwind speculatable willreturn memory(none)
declare float @llvm.minimumnum.f32(float, float) #0

; Function Attrs: nocallback nocreateundeforpoison nofree nosync nounwind speculatable willreturn memory(none)
declare float @llvm.fabs.f32(float) #0

attributes #0 = { nocallback nocreateundeforpoison nofree nosync nounwind speculatable willreturn memory(none) }

define i32 @main(i32 %argc, ptr %argv) {
entry:
  %r = call i32 @test_minimumnum_x_negx(float bitcast (i32 2139095041 to float))
  ret i32 %
```

Ran lli on them, we got different results:

```sh
$ lli src.ll
$ echo$?
127
$ lli tgt.ll
$ echo$?
255
```

> Note: This is a review assisted with a self-built agent. The reproducer was validated manually. Please let me know if anything is wrong.

**Bug Triggering Analysis:**
The provided test case triggers the bug by passing a signaling NaN (sNaN) to `minimumnum(X, -X)`.
In the original code, `minimumnum(sNaN, -sNaN)` is evaluated. According to the semantics of `minimumnum`, it quiets signaling NaNs. So the result is a quiet NaN (qNaN).
In the optimized code, the _expression_ is folded to `-(fabs X)`. `fabs` and `fneg` do not quiet signaling NaNs; they only manipulate the sign bit. So the result is a signaling NaN with its sign bit flipped.
This causes a semantic difference: the original code returns a quiet NaN, while the optimized code returns a signaling NaN. This can lead to unexpected traps or undefined behavior in subsequent operations that expect a quiet NaN.

**Fix Weakness Analysis:**
The fix added `minimumnum` and `maximumnum` to the `IsMinMaxOrXNegX` fold, which transforms `min(X, -X)` to `-(fabs X)` and `max(X, -X)` to `fabs X`.
However, the fix failed to account for the fact that `minimumnum` and `maximumnum` quiet signaling NaNs, whereas `fabs` and `fneg` do not.
By unconditionally applying this fold to `minimumnum` and `maximumnum`, the optimization pass incorrectly preserves signaling NaNs, violating the semantics of the intrinsics.
The test case reveals this weakness by explicitly checking the result of `minimumnum(X, -X)` when `X` is a signaling NaN, demonstrating the semantic mismatch.

Reference: http://llvm.org/docs/LangRef.html#id669

cc @nikic 

_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to