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.

Reply via email to