https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126576
Bug ID: 126576
Summary: isinf() overflow check on a chained floating-point
multiply is deleted at -O1+ without -ffinite-math-only
Product: gcc
Version: 14.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c
Assignee: unassigned at gcc dot gnu.org
Reporter: michaelmalis2 at gmail dot com
Target Milestone: ---
Created attachment 65202
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65202&action=edit
P
## Description
Given two multiplies where the second consumes the first's result, and each is
guarded by `isinf(result) && !isinf(operand)`, gcc deletes the *second* guard.
The first guard is kept. No `-ffast-math` / `-ffinite-math-only` is in effect,
so `finite * finite -> inf` (i.e. overflow) is a case the compiler must model.
The deletion appears to use a fact derived from the first guard: on its
non-error fall-through path the intermediate product is known finite, and gcc
then concludes the second guard's condition is unsatisfiable — which would only
hold if a product of two finite values could not be infinite.
This is a regression: gcc 12 is correct, gcc 13 and later are not.
## Reproducer
The source is self-contained (no headers), so the preprocessed file is
identical
to it. `-save-temps` output attached as `g.i`.
`g.c`:
```c
extern void oflow(void);
double g(double x) {
double r1 = x * x;
if (__builtin_isinf(r1) && !__builtin_isinf(x)) oflow();
double r2 = r1 * 3.0;
if (__builtin_isinf(r2) && !__builtin_isinf(r1)) oflow();
return r2;
}
```
`main.c`:
```c
#include <stdio.h>
#include <stdlib.h>
extern double g(double);
void oflow(void) { printf("oflow() called\n"); exit(1); }
int main(int c, char **v) { printf("g = %g, no call\n", g(atof(v[1]))); return
0; }
```
### Command line and output
```
$ gcc -v -save-temps -O2 g.c main.c -o t
$ ./t 1e154
g = inf, no call <-- WRONG: oflow() must be called
$ gcc -O0 g.c main.c -o t0 && ./t0 1e154
oflow() called <-- expected behaviour
$ ./t 1e200
oflow() called <-- control: the FIRST guard is still emitted
```
With `x = 1e154`: `x*x = 1e308`, which is finite (below `DBL_MAX =
1.7976931348623157e308`); `1e308 * 3.0` overflows to `+inf` with both operands
finite, so the second `oflow()` must be reached.
With `x = 1e200`: `x*x` itself overflows, the first guard fires — showing the
pattern is not wholly removed, only its second instance.
### Generated code
At `-O2` the second multiply is followed directly by the underflow-free return
path; there is no comparison of `r2` against `DBL_MAX` and no call:
`gcc -O2 -c g.c && objdump -d g.o`:
```
4: mov x0, #0x7fefffffffffffff ; DBL_MAX
8: fmov d30, x0 ; gcc lowers isinf(v) to |v| >
DBL_MAX
14: fmul d15, d0, d0 ; r1 = x*x
18: fcmp d15, d30 ; isinf(r1)?
1c: b.le 2c ; finite -> skip guard 1
20: fabs d31, d0 ; |x| <- the !isinf(x) test,
PRESENT
24: fcmp d31, d30
28: b.le 40 ; -> bl oflow
2c: fmov d0, #3.0
30: fmul d0, d15, d0 ; r2 = r1 * 3.0
34: ldr d15, [sp, #16]
38: ldp x29, x30, [sp], #32
3c: ret ; <-- guard 2 absent: no fcmp of r2,
no call
40: bl 0 <oflow>
```
Guard 1 (`14`-`28`) is emitted in full, including the `!isinf(x)` operand test.
Guard 2 is absent: after the second `fmul` at `30` there is no comparison of
`r2` against `DBL_MAX` and no path to `oflow`. Control reaches `2c` only via
`b.le` at `1c`, i.e. only when `r1` is finite, and `3.0` is a finite constant —
so guard 2's condition is satisfiable there and must be tested.
## Affected versions
Verified by runtime witness (does `oflow()` get called for `x = 1e154`?):
| gcc | -O0 | -O1 | -O2 | -O3 |
|------|-----|-----|-----|-----|
| 12.5.0 | ok | ok | ok | ok |
| 13.4.0 | ok | **wrong** | **wrong** | **wrong** |
| 14.2.0 | ok | **wrong** | **wrong** | **wrong** |
| 14.3.0 | ok | **wrong** | **wrong** | **wrong** |
| 15.3.0 | ok | **wrong** | **wrong** | **wrong** |
All on aarch64-linux-gnu (official `gcc:<tag>` container images). Apple clang
17
on arm64 is unaffected at every level. Not yet bisected to a specific commit;
the 12 -> 13 window is the range.
## Ruling out user error
Per https://gcc.gnu.org/bugs/ :
- `gcc -O2 -Wall -Wextra -c g.c` — no diagnostics.
- `-fsanitize=undefined,address` reports **no** undefined behaviour; the wrong
result still occurs at `-O2` with both sanitizers enabled.
- Still reproduces with `-fno-strict-aliasing -fwrapv
-fno-aggressive-loop-optimizations`.
- No `-ffast-math`, `-ffinite-math-only`, `-Ofast`, or
`-funsafe-math-optimizations`.
(For completeness: with `-ffinite-math-only` the deletion is of course
expected
and correct.)
- `-ffp-contract=on` / `=fast` / `=off` make no difference.
## gcc -v
```
Configured with: /usr/src/gcc/configure --build=aarch64-linux-gnu \
--disable-multilib --enable-languages=c,c++,fortran,go
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 14.2.0 (GCC)
```
## Real-world impact
This is PostgreSQL's overflow-detection idiom, verbatim
(`src/include/utils/float.h:207`, `float8_mul`), reached through
`circle_ar() = float8_mul(float8_mul(r, r), M_PI)`
(`src/backend/utils/adt/geo_ops.c:5159`). In stock PostgreSQL 18.3 builds at
the
default `-O2`:
```sql
SELECT area(circle '<(0,0),1e154>'); -- returns Infinity
-- must raise 22003 "value out of range:
overflow"
SELECT area(circle '<(0,0),1e200>'); -- correctly raises 22003
```
Reproduced from an unmodified PostgreSQL 18.3 source tree with gcc 14.2, so it
is not specific to a distribution's patches. A separate report has been filed
with the PostgreSQL project.
## Attachments
- `g.i` (preprocessed, `-save-temps`)
- `main.c`
- `g.s` (`-O2` assembly, for reference)