Two functions with identical bodies can still make different promises
about their arguments, and ICF was not looking at those promises.
get_nonnull_args derives them from two places: the nonnull attribute on
the function type, and a METHOD_TYPE, whose this pointer is nonnull
whether or not anything says so.  sem_function::equals_wpa compared the
type attributes with comp_type_attributes, which by design only reports
attributes whose affects_type_identity is set, and nonnull is not one of
those, and it never compared FUNCTION_TYPE against METHOD_TYPE outside a
devirtualization specific check.  Either way the two functions were
declared equal and unified.  The surviving body keeps its own promise, a
later pass drops the null test that promise makes redundant, and a call
through the other symbol dereferences the null pointer that symbol was
required to accept.

Type identity is the wrong question for ICF.  The comment above the
variable case already says that for functions we do not know which
attributes affect code generation, which is why the decl attributes are
compared as whole lists.  Compare the type attributes the same way, and
reject a METHOD_TYPE paired with a FUNCTION_TYPE, which carries the same
promise with nothing on the attribute list to compare.

        PR ipa/123227

gcc/ChangeLog:

        * ipa-icf.cc (sem_function::equals_wpa): Compare TYPE_ATTRIBUTES
        as a list rather than through comp_type_attributes.  Reject a
        METHOD_TYPE compared against a FUNCTION_TYPE.

gcc/testsuite/ChangeLog:

        * gcc.dg/ipa/pr123227.c: New test.
        * g++.dg/ipa/pr123227.C: New test.
        * gcc.dg/lto/pr123227_0.c: New test.
        * gcc.dg/lto/pr123227_1.c: New test.
        * g++.dg/lto/pr123227_0.C: New test.
        * g++.dg/lto/pr123227_1.C: New test.

Signed-off-by: Rohith Kapelli <[email protected]>
---

Both spellings are in the PR: a C case with an explicit nonnull attribute,
and the reporter's original C++ case where a non-static member function is
unified with a free function.

The patch compares the type attributes as whole lists rather than through
comp_type_attributes, and rejects a METHOD_TYPE paired with a
FUNCTION_TYPE. Type identity is the wrong question for ICF.  The comment
already in the file says so: "For functions we compare attributes in
equals_wpa, because we do not know what attributes may cause codegen
differences".  That is why the DECL_ATTRIBUTES comparison immediately below
already compares whole lists with attribute_list_equal.  This change makes
the type attributes agree with the policy the file already states.

Comment 7 and comment 8 of the PR enumerate fifteen attributes that set
affects_type_identity false and can still affect generated code: nonnull,
nonnull_if_nonzero, warn_unused_result, sentinel, "fn spec",
returns_nonnull, alloc_align, assume_aligned, access, fd_arg, fd_arg_read,
fd_arg_write, null_terminated_string_arg, reproducible and unsequenced.
All fifteen are false in the 7th field of their entries in
gcc/c-family/c-attribs.cc, so comp_type_attributes skips every one of them.
Comparing the type attributes as a list covers the set.

Of the fourteen that can be written in source, one is exploitable today.
Three never reach ICF, because the attribute is consumed before it runs and
the bodies already differ.  Ten merge and stay correct, because they
constrain what a caller may assume rather than what the body may assume:
after a merge each symbol keeps its own declaration, which the dump shows
directly, returns_nonnull still on one decl and absent from the other.

That count is a fact about pass ordering, not about what ICF may ignore.
nonnull and nonnull_if_nonzero both constrain what the body may assume
about its parameters, which is the thing a merge shares, and the second is
safe only because another pass consumes it first.  An equality predicate
whose correctness depends on which attributes happen to be consumed before
it runs is the wrong shape for this pass.

I also implemented the narrower form, which keeps comp_type_attributes and
adds a comparison of the two get_nonnull_args bitmaps.  It is bootstrapped
with the same checking options and fully regression tested, and I can post
it on request.  It fixes nonnull and leaves the other fourteen attributes
as they are.  The two forms are indistinguishable in cost.  Merge counts
and code size, unpatched trunk against this patch against the narrow form:

  libstdc++ sources built into one shared library with LTO, merge counts read
  from the WPA stage dump, size is .text of the stripped library:
    -O2   182 unified, 255 equal symbols, 1707315 bytes, all three identical
    -Os   190 unified, 270 equal symbols, 1364990 bytes, all three identical
  2331 file testsuite corpus at -O2, gcc.c-torture/execute plus g++.dg/opt and
  g++.dg/ipa, summing per file counts:
    393 equal symbols, 3112301 text bytes, all three identical
  the existing ICF test set, comparing merge decisions rather than pass counts:
    177 decision lines, byte identical across all three
  400 mutually mergeable functions each carrying nine of those attributes, which
  is the adversarial case for the quadratic comparison:
    Equal symbols: 399 in every configuration, so every pair reaches the
    attribute comparison

WPA link time on the same libstdc++ workload, timing only the link step,
measured round robin across the three compilers over twelve rounds on an
idle machine:

    trunk   min 23.726  median 24.056  mean 24.030  max 24.214  stdev 0.151
    patch   min 23.659  median 24.056  mean 24.021  max 24.180  stdev 0.154
    narrow  min 23.789  median 24.053  mean 24.026  max 24.167  stdev 0.128

Median difference is -0.001 s for this patch and -0.003 s for the narrow
form, against 0.465 s of drift in the machine itself over the twelve
rounds.

The adversarial case for the quadratic comparison is 400 functions with
byte identical bodies each carrying N of those attributes, address taken so
none is removed, which puts all 400 in one congruence class so every pair
reaches the attribute comparison.  N scaled 0, 1, 3, 9, nine being the
largest set of those attributes simultaneously legal on one signature, so
an 81 to 1 ratio in the quadratic term.  Seven rounds per scale, medians in
seconds:

    attrs   trunk      patch      narrow   patch-trunk
        0   0.585      0.601      0.604        +0.016
        1   0.612      0.609      0.615        -0.003
        3   0.537      0.539      0.534        +0.002
        9   0.537      0.532      0.535        -0.005

The delta does not grow with the attribute count, and the spread across the
28 trunk samples alone is 0.101 s, six times the largest delta.

The patch does not block merging that should happen.  Two static member
functions, which have FUNCTION_TYPE, still merge; two member functions of
the same class and two virtual functions, both METHOD_TYPE, still merge;
and two functions carrying the same attributes in the opposite order still
merge, since attribute_list_equal is order insensitive by construction.
Each of those is a testcase, not an assertion.

Testing, on aarch64-unknown-linux-gnu at trunk 8df013222df: patched and
unpatched from the same tree in the same environment, with only ipa-icf.cc
differing, three stage bootstrap configured --enable-languages=c,c++,lto
--enable-checking=yes,rtl,extra --disable-multilib --disable-nls
--disable-libsanitizer, make compare reported comparison successful.  Shard
sums merged with contrib/dg-extract-results.sh and compared with
contrib/compare_tests:

    gcc         399733 pass, 19 fail  ->  399737 pass, 15 fail
    g++         466938 pass,  8 fail  ->  467042 pass,  0 fail
    gcc.dg/lto    1867 pass,  2 fail  ->    1869 pass,  0 fail
    g++.dg/lto    1747 pass,  2 fail  ->    1749 pass,  0 fail
    libstdc++    18889 pass, 14 fail  ->   18889 pass, 14 fail
    libgomp       6315 pass,  0 fail  ->    6315 pass,  0 fail
    libitm          44 pass,  0 fail  ->      44 pass,  0 fail
    libatomic       54 pass,  0 fail  ->      54 pass,  0 fail

Unresolved, error and xpass are 0 everywhere before and after, and
compare_tests reports no "Tests that now fail, but worked before" section
for any comparison. Across the four runtime libraries every result line is
identical, 26919 of them, same md5 sorted; the 14 libstdc++ failures are
pre-existing mdspan dg-error line mismatches present in both runs.  The 12
tests that change from fail to pass are this patch's own; the 15 that
remain are pre-existing, mostly aarch64 SVE scan-assembler and vectoriser
dump scans.  The g++ comparison also reports 96 new and 96 disappeared
tests, all g++.dg/modules, whose dejagnu names embed a generated gcm.cache
path; that suite run twice against one unchanged compiler differs by 27 new
and 42 disappeared, so the churn carries no information.  The testsuite
rerun with the bootstrapped compiler itself gives the same 15 failures.

The tests.  gcc.dg/ipa/pr123227.c and g++.dg/ipa/pr123227.C cover the
non-LTO path; gcc.dg/lto/pr123227_{0,1}.c and g++.dg/lto/pr123227_{0,1}.C
are dg-lto-do run tests that exercise the WPA path this patch guards.  Each
asserts both that no merge was materialised, scan-ipa-dump-not "Unified",
and that ICF found no equal pair, scan-ipa-dump "Equal symbols: 0", using
the scan-wpa-ipa-dump forms in the LTO tests; neither assertion depends on
which form of the fix is applied. In the LTO tests the two functions live
in different translation units and both are called, so both survive to WPA.
On unpatched trunk the WPA dump reports Equal symbols: 1 and "Unified;
Function alias has been created." and the program segfaults; with the patch
it reports 0, prints no Unified line, and exits 0. All four tests fail
before the patch and pass after, 18 FAIL to 0 FAIL, on trunk and on each of
the four release branches.

Branch applicability.  This is a wrong-code regression affecting 13, 14,
15, 16 and 17.  The patch applies cleanly to trunk and to the gcc-16,
gcc-15, gcc-14 and gcc-13 branches.  I built each of the four branches
unpatched, ran the four testcases, applied only the gcc/ipa-icf.cc hunk,
rebuilt in place and reran them:

    releases/gcc-16  38b61f4fee1e   12 fail 10 pass  ->  0 fail 22 pass
    releases/gcc-15  ca85bd3e8a1f   12 fail 10 pass  ->  0 fail 22 pass
    releases/gcc-14  b0a1bcacd1c1   14 fail 11 pass  ->  0 fail 25 pass
    releases/gcc-13  0a4ef247dd72   14 fail 11 pass  ->  0 fail 25 pass

The differing line counts are the number of -std variants each branch's g++
harness runs.  I have left the backport decision to the maintainers.

OK for trunk?

 gcc/ipa-icf.cc                        | 16 +++++++--
 gcc/testsuite/g++.dg/ipa/pr123227.C   | 50 ++++++++++++++++++++++++++
 gcc/testsuite/g++.dg/lto/pr123227_0.C | 52 +++++++++++++++++++++++++++
 gcc/testsuite/g++.dg/lto/pr123227_1.C | 24 +++++++++++++
 gcc/testsuite/gcc.dg/ipa/pr123227.c   | 45 +++++++++++++++++++++++
 gcc/testsuite/gcc.dg/lto/pr123227_0.c | 47 ++++++++++++++++++++++++
 gcc/testsuite/gcc.dg/lto/pr123227_1.c | 17 +++++++++
 7 files changed, 248 insertions(+), 3 deletions(-)
 create mode 100644 gcc/testsuite/g++.dg/ipa/pr123227.C
 create mode 100644 gcc/testsuite/g++.dg/lto/pr123227_0.C
 create mode 100644 gcc/testsuite/g++.dg/lto/pr123227_1.C
 create mode 100644 gcc/testsuite/gcc.dg/ipa/pr123227.c
 create mode 100644 gcc/testsuite/gcc.dg/lto/pr123227_0.c
 create mode 100644 gcc/testsuite/gcc.dg/lto/pr123227_1.c

diff --git a/gcc/ipa-icf.cc b/gcc/ipa-icf.cc
index 44501ec760e..64cc722bd7a 100644
--- a/gcc/ipa-icf.cc
+++ b/gcc/ipa-icf.cc
@@ -659,10 +659,20 @@ sem_function::equals_wpa (sem_item *item,
     return return_false_with_msg ("different number of references");
 
   /* Checking function attributes.
-     This is quadratic in number of attributes  */
-  if (comp_type_attributes (TREE_TYPE (decl),
-                           TREE_TYPE (item->decl)) != 1)
+     This is quadratic in number of attributes.
+     comp_type_attributes only considers attributes that affect type
+     identity, but an attribute that leaves the type alone can still let
+     the body assume something, nonnull being one, so compare the lists
+     the same way the decl attributes are compared below.  */
+  if (!attribute_list_equal (TYPE_ATTRIBUTES (TREE_TYPE (decl)),
+                            TYPE_ATTRIBUTES (TREE_TYPE (item->decl))))
     return return_false_with_msg ("different type attributes");
+  /* A METHOD_TYPE promises a nonnull this pointer without carrying an
+     attribute that says so, so it is not interchangeable with a
+     FUNCTION_TYPE that makes no such promise.  */
+  if ((TREE_CODE (TREE_TYPE (decl)) == METHOD_TYPE)
+      != (TREE_CODE (TREE_TYPE (item->decl)) == METHOD_TYPE))
+    return return_false_with_msg ("METHOD_TYPE and FUNCTION_TYPE mismatch");
   if (!attribute_list_equal (DECL_ATTRIBUTES (decl),
                             DECL_ATTRIBUTES (item->decl)))
     return return_false_with_msg ("different decl attributes");
diff --git a/gcc/testsuite/g++.dg/ipa/pr123227.C 
b/gcc/testsuite/g++.dg/ipa/pr123227.C
new file mode 100644
index 00000000000..180792ba247
--- /dev/null
+++ b/gcc/testsuite/g++.dg/ipa/pr123227.C
@@ -0,0 +1,50 @@
+// PR ipa/123227
+// { dg-do run }
+// { dg-options "-Os -fipa-icf -fdump-ipa-icf-details" }
+
+// Link::get_vals and get_vals have identical bodies, but the member
+// function may assume a non-null this while the free function must accept
+// a null argument, so ICF must not unify them.
+
+enum Val { zero = 0 };
+
+inline Val&
+operator|=(Val& a, Val b)
+{
+    return a = static_cast<Val>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+struct Link {
+    Val get_vals();
+    Val val;
+    Link* next;
+};
+
+Val
+Link::get_vals()
+{
+    Val v = zero;
+    for (Link* l = this; l; l = l->next)
+        v |= l->val;
+    return v;
+}
+
+Val
+get_vals(Link* l)
+{
+    Val v = zero;
+    for (; l; l = l->next)
+        v |= l->val;
+    return v;
+}
+
+int
+main(int, char**)
+{
+    if (get_vals(0) != zero)
+        __builtin_abort();
+    return 0;
+}
+
+// { dg-final { scan-ipa-dump-not "Unified" "icf" } }
+// { dg-final { scan-ipa-dump "Equal symbols: 0" "icf" } }
diff --git a/gcc/testsuite/g++.dg/lto/pr123227_0.C 
b/gcc/testsuite/g++.dg/lto/pr123227_0.C
new file mode 100644
index 00000000000..2072c598102
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/pr123227_0.C
@@ -0,0 +1,52 @@
+// PR ipa/123227
+// { dg-lto-do run }
+// { dg-lto-options {{-Os -flto -fipa-icf -fdump-ipa-icf-details}} }
+
+// Link::get_vals and get_vals have identical bodies, but the member
+// function may assume a non-null this while the free function must accept
+// a null argument, so ICF must not unify them at WPA.  Both are called so
+// that neither is removed before ICF runs, and both are noinline so that
+// the calls survive as calls.
+
+enum Val { zero = 0 };
+
+inline Val&
+operator|=(Val& a, Val b)
+{
+    return a = static_cast<Val>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+struct Link {
+    Val get_vals();
+    Val val;
+    Link* next;
+};
+
+Val __attribute__((noinline))
+Link::get_vals()
+{
+    Val v = zero;
+    for (Link* l = this; l; l = l->next)
+        v |= l->val;
+    return v;
+}
+
+extern Val get_vals(Link* l);
+
+static Link one = { Val(5), 0 };
+
+// Volatile so that the null argument is not propagated into get_vals.
+Link *volatile nullp = 0;
+
+int
+main()
+{
+    if (one.get_vals() != Val(5))
+        __builtin_abort();
+    if (get_vals(nullp) != zero)
+        __builtin_abort();
+    return 0;
+}
+
+// { dg-final { scan-wpa-ipa-dump-not "Unified" "icf" } }
+// { dg-final { scan-wpa-ipa-dump "Equal symbols: 0" "icf" } }
diff --git a/gcc/testsuite/g++.dg/lto/pr123227_1.C 
b/gcc/testsuite/g++.dg/lto/pr123227_1.C
new file mode 100644
index 00000000000..bc2a7aa168d
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/pr123227_1.C
@@ -0,0 +1,24 @@
+// PR ipa/123227
+
+enum Val { zero = 0 };
+
+inline Val&
+operator|=(Val& a, Val b)
+{
+    return a = static_cast<Val>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+struct Link {
+    Val get_vals();
+    Val val;
+    Link* next;
+};
+
+Val __attribute__((noinline))
+get_vals(Link* l)
+{
+    Val v = zero;
+    for (; l; l = l->next)
+        v |= l->val;
+    return v;
+}
diff --git a/gcc/testsuite/gcc.dg/ipa/pr123227.c 
b/gcc/testsuite/gcc.dg/ipa/pr123227.c
new file mode 100644
index 00000000000..e1942350819
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/ipa/pr123227.c
@@ -0,0 +1,45 @@
+/* PR ipa/123227 */
+/* { dg-do run } */
+/* { dg-options "-Os -fipa-icf -fdump-ipa-icf-details" } */
+
+/* The two functions have identical bodies but only one promises a non-null
+   argument, so ICF must not unify them: the surviving body is allowed to
+   drop the null test that the other one needs.  */
+
+typedef struct Link Link;
+struct Link
+{
+  int val;
+  Link *next;
+};
+
+int get_vals_nonnull (Link *) __attribute__((nonnull (1)));
+
+int
+get_vals_nonnull (Link *l)
+{
+  int v = 0;
+  for (; l; l = l->next)
+    v |= l->val;
+  return v;
+}
+
+int
+get_vals (Link *l)
+{
+  int v = 0;
+  for (; l; l = l->next)
+    v |= l->val;
+  return v;
+}
+
+int
+main (void)
+{
+  if (get_vals ((Link *) 0) != 0)
+    __builtin_abort ();
+  return 0;
+}
+
+/* { dg-final { scan-ipa-dump-not "Unified" "icf" } } */
+/* { dg-final { scan-ipa-dump "Equal symbols: 0" "icf" } } */
diff --git a/gcc/testsuite/gcc.dg/lto/pr123227_0.c 
b/gcc/testsuite/gcc.dg/lto/pr123227_0.c
new file mode 100644
index 00000000000..60f2d730305
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lto/pr123227_0.c
@@ -0,0 +1,47 @@
+/* PR ipa/123227 */
+/* { dg-lto-do run } */
+/* { dg-lto-options {{-Os -flto -fipa-icf -fdump-ipa-icf-details}} } */
+
+/* get_vals_nonnull and get_vals have identical bodies but only one
+   promises a non-null argument, so ICF must not unify them at WPA: the
+   surviving body is allowed to drop the null test the other one needs.
+   Both are called so that neither is removed before ICF runs, and both
+   are noinline so that the calls survive as calls.  */
+
+typedef struct Link Link;
+struct Link
+{
+  int val;
+  Link *next;
+};
+
+int get_vals_nonnull (Link *) __attribute__((nonnull (1)));
+
+int __attribute__((noinline))
+get_vals_nonnull (Link *l)
+{
+  int v = 0;
+  for (; l; l = l->next)
+    v |= l->val;
+  return v;
+}
+
+extern int get_vals (Link *);
+
+static Link one = { 5, 0 };
+
+/* Volatile so that the null argument is not propagated into get_vals.  */
+Link *volatile nullp = 0;
+
+int
+main (void)
+{
+  if (get_vals_nonnull (&one) != 5)
+    __builtin_abort ();
+  if (get_vals (nullp) != 0)
+    __builtin_abort ();
+  return 0;
+}
+
+/* { dg-final { scan-wpa-ipa-dump-not "Unified" "icf" } } */
+/* { dg-final { scan-wpa-ipa-dump "Equal symbols: 0" "icf" } } */
diff --git a/gcc/testsuite/gcc.dg/lto/pr123227_1.c 
b/gcc/testsuite/gcc.dg/lto/pr123227_1.c
new file mode 100644
index 00000000000..677f59a918c
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lto/pr123227_1.c
@@ -0,0 +1,17 @@
+/* PR ipa/123227 */
+
+typedef struct Link Link;
+struct Link
+{
+  int val;
+  Link *next;
+};
+
+int __attribute__((noinline))
+get_vals (Link *l)
+{
+  int v = 0;
+  for (; l; l = l->next)
+    v |= l->val;
+  return v;
+}
-- 
2.53.0

Reply via email to