https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128240
Bug ID: 128240
Summary: tree-loop-im hoists a guarded global load out of the
loop
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Keywords: wrong-code
Severity: normal
Priority: P3
Component: tree-optimization
Assignee: unassigned at gcc dot gnu.org
Reporter: 220255624 at seu dot edu.cn
Target Milestone: ---
Host: x86_64-pc-linux-gnu
Target: x86_64-pc-linux-gnu
Build: x86_64-pc-linux-gnu
Created attachment 66001
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=66001&action=edit
reproducer t.c (self-contained; includes the mprotect harness)
Overview:
GCC's tree-loop-im pass hoists a global load out of a loop even though the
load is guarded
by a branch that is taken at run time, so the produced binary performs a
global read that
the source never performs on that path. Placed on a page mapped PROT_NONE
this becomes a
SIGSEGV; under concurrency it is an extra fetch window (TOCTTOU / double
fetch).
Steps to Reproduce:
1) save the attached reproducer as t.c
2) gcc -O0 t.c -o t && ./t # NOFAULT side=0
gcc -O1 t.c -o t && ./t # SIGSEGV, si_addr == &g_t
gcc -O1 -fno-tree-loop-im t.c -o t && ./t # NOFAULT (only flag that
helps)
gcc -O2 t.c -o t && ./t # NOFAULT on 15.2/16.2/17.0.0
3) ./t r makes the page readable again and the program completes, which shows
the fault
comes from the protection of g_t's own page and not from a wild pointer.
Actual Results:
At -O1 the binary loads g_t before the guard test and outside the loop:
mov 0x4d96(%rip),%esi # g_guard
movslq 0x3d77(%rip),%r8 # g_t <-- hoisted, unconditional
test %esi,%esi
jne ... # the guard is only tested afterwards
so the SIGSEGV handler reports si_addr == &g_t (0x405000 in my build).
g_t owns a whole page: objdump -h reports safe_section as exactly 0x1000
bytes at 2**12
alignment and nm shows g_t as the only symbol in it (next symbol at
0x406000), so a fault
on that page can only be a fault on g_t.
-fdump-tree-lim2-details states the transformation:
Moving statement _2 = g_t[0];
(cost 20) out of loop 1.
Expected Results:
g_guard is 1 at run time, so the source returns on the first iteration and
never evaluates
g_t[0]; the program must not touch g_t's page at any -O level.
Build Date & Hardware:
gcc 16.2.0, x86_64-pc-linux-gnu, configured with
../configure --prefix=... --disable-multilib --enable-languages=c,c++
--disable-bootstrap.
Reproduced at -O1 on 13.3.0, 14.2.0, 15.2.0, 16.2.0 and 17.0.0 20261018; on
13.3.0 and
14.2.0 it also fails at -O2/-O3/-Os, while 15.2.0/16.2.0/17.0.0 are clean at
those levels.
clang 18.1.3/19.1.1/20.1.2 are clean at every level. I did not bisect, so I
do not claim
this is a regression.
Additional Information:
- -fno-tree-loop-im is the only flag among the ones I tried that removes the
fault.
-fno-move-loop-invariants does NOT help (that is the RTL pass), and neither
do
-fno-tree-pre / -fno-tree-fre / -fno-tree-vrp / -fno-tree-dominator-opts /
-fno-tree-dse / -fno-tree-cselim. That is also the evidence that this is a
different
problem from the RTL hoisting reports below.
- Replacing the loop by a single guarded statement (same guard, same guarded
load, one
iteration) does not fault on 13.3-17.0.0 at any of -O1/-O2/-O3/-Os, i.e.
the loop is
required for this transformation.
- PR45223 ("RTL PRE GCSE pass hoists trapping insn out of loop", wrong-code,
moxie-elf,
RESOLVED FIXED in 4.5.2) establishes that hoisting a possibly-faulting insn
out of a loop
is wrong-code. The present report is the tree-level analogue on x86-64, and
it is still
present in 13.3 through 17.0.0. Here -fno-move-loop-invariants does not
help, so it is not
the same pass.
- PR127601 (vectorizer introduces reads outside a page boundary, ASSIGNED,
wrong-code) uses
the same observability argument in a different pass; PR122868 (fixed) is
the vectorizer
pulling loop invariant loads across loop exits, with -fno-tree-vectorize as
the workaround.
PR41026/PR19347 are missed-optimization reports in the opposite direction.
- This is not PR128229 either. That report is the RTL-level twin and is
removed by
-fno-move-loop-invariants; no UNSPEC or trapping arithmetic is involved
here, so it cannot
be dismissed as a target issue.
- Attaching t.c here; t.i (gcc -save-temps output) and the -v configuration
log follow as
separate attachments.