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.