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.

Reply via email to