https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128152

            Bug ID: 128152
           Summary: virtual call through a member with virtual bases
                    folded to __builtin_unreachable at -O2
           Product: gcc
           Version: 17.0
            Status: UNCONFIRMED
          Keywords: wrong-code
          Severity: normal
          Priority: P3
         Component: ipa
          Assignee: unassigned at gcc dot gnu.org
          Reporter: yqtian668 at gmail dot com
  Target Milestone: ---

Created attachment 65951
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65951&action=edit
Preprocessed testcase; virtual call through member virtual base folded to
unreachable

At -O2, GCC folds a valid virtual call through a virtual base of an ordinary
struct member to __builtin_unreachable. The generated executable terminates
with SIGILL instead of printing 726.

Reproducer:

#include <cstdio>
static volatile unsigned in[] = {1u};
namespace {
struct A {
  unsigned x = 1;
  virtual ~A() {}
  virtual unsigned f() { return 242u; }
};
struct B : virtual A {};
struct C : virtual A, virtual B {
  unsigned y = 2;
  unsigned f() override { return y * 278u + 188u; }
};
struct D : virtual A, virtual C {
  unsigned f() override { return 242u; }
};
__attribute__((noinline)) unsigned call(C& r) { return r.f(); }
}
struct Hold { unsigned pad = 5u; D m; };
int main() {
  unsigned sum = 0;
  C* c = new C;
  A* d = new D;
  Hold h;
  for (unsigned i = 0; i < in[0] % 2u + 1u; ++i)
    sum += d->f();
  sum += call(h.m);
  delete c;
  delete d;
  std::printf("%u\n", sum);
}

Command:

g++ -std=c++14 -Wall -Wextra -D_GLIBCXX_ASSERTIONS -O2 testcase.cpp -o test
./test

Actual behavior:

Compilation succeeds without a source-level diagnostic. The executable
terminates with SIGILL (exit status 132) and prints nothing. Adding
-fopt-info-optimized reports:

testcase.cpp:17:59: optimized: converting indirect call in unsigned int
call.constprop(C&) to direct call to void __builtin_unreachable()

Expected behavior:

The executable should print 726 and exit normally. The loop calls D::f() twice
through A* and contributes 2 * 242. The call call(h.m) converts the D member to
its virtual C base, but the final overrider remains D::f() and contributes
another 242.

The testcase uses ordinary object lifetimes. The deletion through A* is valid
because A has a virtual destructor. The arithmetic is unsigned. At -O1 with
UBSan and ASan, the executable prints 726 without a sanitizer diagnostic. Clang
trunk at -O2 also prints 726. GCC prints 726 when either -fno-ipa-cp or
-fno-devirtualize is added.

Regression evidence:

GCC 4.9 prints 726 at -O2. GCC 5 through GCC 15 instead terminate with SIGSEGV,
while GCC 16 and the trunk compiler below terminate with SIGILL.

PR128086 has a similar unreachable fold involving virtual bases. Its first bad
commit is 6f8091fc3ed. Both that commit and its parent 8e1ba78f1b8 print 726
for this testcase. On current trunk, -fno-ipa-cp fixes this testcase but does
not fix the reduced PR128086 testcase.

Suspected cause:

The IPA-CP dump for the edge from main to call(C&) records this polymorphic
context:

Outer type:struct Hold offset 256

The offset is correct: m begins at byte 8, and C is a virtual base of D at byte
24 within the complete D object. IPA-CP specializes call for this context,
obtains an empty target list, and replaces the virtual call with
__builtin_unreachable. The failure appears to occur while
ipa_polymorphic_call_context::restrict_to_inner_class walks from Hold through
its D member to the virtual C base.

Compiler version:

g++ (GCC) 17.0.0 20261021 (experimental)
Target: x86_64-pc-linux-gnu
Configured with: /src/configure --prefix=/opt/gcc --enable-languages=c,c++
--disable-multilib --disable-bootstrap --enable-checking=yes
Thread model: posix

Verified with GCC trunk commit 8cc6e7178c2e3b6156d7992bd18285be0da3c92c.

Reply via email to