https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127917
Bug ID: 127917
Summary: wrong code at -O3: IPA-CP redirects a recursive call
to a clone made for a different polymorphic context
Product: gcc
Version: 16.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: ipa
Assignee: unassigned at gcc dot gnu.org
Reporter: hugo at hsal dot es
Target Milestone: ---
Created attachment 65856
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65856&action=edit
testcase, aborts at -O3 with 16.1 and 16.2
This program aborts at -O3 with GCC 16.1 and 16.2 on x86_64-linux:
struct Visitor {
virtual void visit() {}
};
struct Trap : Visitor {
void visit() override { __builtin_abort(); }
};
struct Node {
Node *left, *right;
};
struct Tree {
Node *root;
virtual int walk(Visitor &visitor) { return walk(root, visitor); }
// Returns the number of leaves, calling visitor.visit() on each.
int walk(Node *node, Visitor &visitor) {
if (!node->left) {
visitor.visit();
return 1;
}
return walk(node->left, visitor) + walk(node->right, visitor);
}
};
// noipa hides the dynamic type of `tree`, so both walk calls stay speculative.
// main passes rounds = 0: Trap::visit must never run.
__attribute__((noipa)) void check(Tree &tree, int rounds) {
Visitor silent;
if (tree.walk(silent) != 4)
__builtin_abort();
Trap trap;
for (int i = 0; i < rounds; i++)
tree.walk(trap);
}
int main() {
Node leaves[4] = {};
Node inner[2] = {{&leaves[0], &leaves[1]}, {&leaves[2], &leaves[3]}};
Node root = {&inner[0], &inner[1]};
Tree tree;
tree.root = &root;
check(tree, 0);
}
$ g++ -O3 t.cc && ./a.out
Aborted
$ g++ -O3 -fno-ipa-cp-clone t.cc && ./a.out
$
main passes rounds = 0, so Trap::visit should never run. The program passes
with GCC 14.4, GCC 15.3 and Clang 21, at -O2, with -fno-ipa-cp-clone, and under
-fsanitize=address,undefined.
Trunk (17.0.0 20261014) seems to use a different cloning threshold and passes
the file above, but the bug is still there. The second attachment,
testcase-all.cc, has a similar shape and aborts at -O3 with 16.1, 16.2 and
trunk. It's the simplest version I found that hits both thresholds:
struct Selector { virtual void Accept() {} };
struct Tree {
virtual int Select(Selector &s) { Select(depth, s); return 1; }
void Select(int d, Selector &s) {
if (d) { Select(d - 1, s); Select(d - 1, s); } else s.Accept();
}
int depth = 2;
};
struct Bad : Selector { void Accept() { __builtin_abort(); } };
__attribute__((noipa)) void perform(Tree &t) {
Selector ok;
if (t.depth != 1 && t.Select(ok)) return;
Bad bad;
t.Select(bad); t.Select(bad);
}
int main() { Tree t; perform(t); }
Compilers:
- gcc version 16.2.0 (GCC), from nixpkgs 8c0bdb227890
- gcc version 16.1.0 (GCC), from nixpkgs ec830498ebdf
- gcc version 17.0.0 20261014 (experimental), master 5f39942ffd6
With 16.2, -fdump-ipa-cp-details shows this (the /NN are cgraph node numbers):
- First sweep: the loop makes the Trap call hot, so IPA-CP clones the wrapper
(/29) and walk(Node *, Visitor &) (/30) for Trap, then walk for the `this`
context (/31). /30 is the only caller of /31 at that point, so /31 also gets
the Trap context, and visitor.visit() in it is devirtualized to Trap::visit.
- Second sweep: the silent call evaluated to 1422, under the first sweep's
threshold of 1500, so its Visitor clones are only made now (/32, /33, /34).
Then:
- adding an extra caller int Tree::walk.constprop(Node*, Visitor&)/34 of int
Tree::walk.constprop(Node*, Visitor&)/31
which sends the recursive call in the Visitor clone (/34) to the Trap clone
(/31).
perhaps_add_new_callers checks the value, known scalars and aggregate values of
the target clone, but not its known polymorphic contexts. GCC 15 has the same
checks, but it clones in a single sweep, so a later clone is never redirected
into an earlier one for a different context.
I bisected without building GCC, using prebuilt snapshots from Compiler
Explorer and Fedora rawhide. The last good one is Fedora's trunk snapshot from
2026-01-13, which had 3fd5a1e76bb and e58b11cd6c9 temporarily reverted. The
first bad one is their 2026-01-15 snapshot, which dropped that revert. Between
the two, every dump before IPA-CP is identical and the IPA-CP dump is the first
to differ, so the change is inside IPA-CP. The IPA-CP commits in that range are
3fd5a1e76bb, e58b11cd6c9 and 946580776e8 (PR123542). The most likely cause is
r16-6651 (3fd5a1e76bb, "ipa-cp: Better opportunity evaluation and ranking"),
together with the extra sweeps added earlier in r16-6538 (cfc746c2118). On
16.2, testcase-all.cc passes with --param=ipa-cp-sweeps=1 or 2 and fails with 3
(the default) or 4.
I found this in OpenCascade 7.9.3. BRepClass3d_SClassifier::Perform walks a
bounding box tree with a point selector, then with a line selector, through the
recursive NCollection_UBTree::Select. GCC 16 runs the line selector's Accept on
the point selector, so classifying any point on an edge or vertex of a solid
segfaults, and FreeCAD booleans crash.
I have a patch that adds a polymorphic context check to
perhaps_add_new_callers. On trunk it fixes testcase-all.cc and OpenCascade.
I'll send it to gcc-patches once a bootstrap and regtest finish.