On Sat, Oct 10, 2026 at 7:06 AM Kees Cook <[email protected]> wrote: > > With -fno-strict-aliasing, IPA ICF merges functions that copy the same > fields between two struct types of different sizes, and the merged body, > inlined at the other function's call sites, can trip diagnostics like > -Warray-bounds: > > struct small { char pad[16]; int a; unsigned int b, c; }; > struct big { char pad[16]; int a; unsigned int b, c; int extra; }; > > void small_to_big (struct big *dst, const struct small *src) > { > dst->a = src->a; > ... > } > > void big_to_small (struct small *dst, const struct big *src) > { > dst->a = src->a; > ... > } > > void f (struct big *p) > { > struct small tmp; > > big_to_small (&tmp, p); > ... > } > > In function 'small_to_big', inlined from 'f' at t.c:22:3: > warning: array subscript 'struct big[0]' is partly outside array bounds > of 'struct small[1]' [-Warray-bounds=]
I think these diagnostics are dubious with -fno-strict-aliasing which essentially says you can access every object via any other objects type. > Without TBAA, compare those sizes at each level of the path, so we don't > see mismatched sizes on a merged body. Require the same tree codes at > each level too, as the TBAA comparison does, and array members of the > same kind: -Warray-bounds treats an array member that ends its object as > possibly flexible, so "int arr[1]" at the end of one struct and in the > middle of another, at the same offset in structs of the same size, must > not merge either. The sizes are compared with ICF's operand_equal_p, so > those of variably modified types, which refer to each function's own > decls, still match. Functions accessing distinct types of the same > layout still merge, as gcc.dg/lto/pr113359-*.c tests expect. > > The Linux kernel (which uses -fno-strict-aliasing) hit this with GCC 16 in > net/ipv6/ip6_tunnel.c: ip6_tnl_parm_from_user() and ip6_tnl_parm_to_user() > copy the same fields between struct ip6_tnl_parm and the larger struct > __ip6_tnl_parm in opposite directions. This was only seen on m68k, > parisc, arc, xtensa, and openrisc. Those functions also memcpy a char > array member, and since r17-4004 (PR119006) ICF compares the fields in > those addresses and keeps the two apart, but functions that copy only > the scalar fields (as Linux is) still merged. > > The stricter comparison doesn't cost merges in the kernel code I > measured: across the 636 objects of net/ipv6, fs/ext4, kernel, and mm in > an x86 allmodconfig build, none of the 1569 ICF merges is between > accesses whose sizes or array kinds differ, so the count and the text > size are the same with and without this change. > > This turned out to match PR105438, but on trunk (with -fstrict-aliasing) > the access path comparison refused the merge. But with -fno-strict-aliasing > it was exposed. (Its testcase also now needed -fno-early-inlining, or both > instantiations were inlined before ICF runs.) So instead of this I'd rather work towards inlining the original body rather than the ICF prevailing one. And force-disable these kind of diagnostics with -fno-strict-aliasing? Richard. > > Assisted-by: LLM [tests] > > gcc/ChangeLog: > > PR ipa/105438 > * tree-ssa-alias.cc (ao_compare::compare_ao_refs): Without TBAA, > compare the tree codes, the sizes of the types, and the kinds of > array members along the access paths. > > gcc/testsuite/ChangeLog: > > PR ipa/105438 > * g++.dg/ipa/pr105438.C: New test. > * gcc.dg/ipa/ipa-icf-41.c: New test. > * gcc.dg/ipa/ipa-icf-42.c: New test. > > Signed-off-by: Kees Cook <[email protected]> > --- > I tried to copy neighboring style. I don't like "while (true)", but that > seemed to be idiomatic? > --- > gcc/testsuite/g++.dg/ipa/pr105438.C | 33 ++++++++++++ > gcc/testsuite/gcc.dg/ipa/ipa-icf-41.c | 73 ++++++++++++++++++++++++++ > gcc/testsuite/gcc.dg/ipa/ipa-icf-42.c | 75 +++++++++++++++++++++++++++ > gcc/tree-ssa-alias.cc | 34 +++++++++++- > 4 files changed, 213 insertions(+), 2 deletions(-) > create mode 100644 gcc/testsuite/g++.dg/ipa/pr105438.C > create mode 100644 gcc/testsuite/gcc.dg/ipa/ipa-icf-41.c > create mode 100644 gcc/testsuite/gcc.dg/ipa/ipa-icf-42.c > > diff --git a/gcc/testsuite/g++.dg/ipa/pr105438.C > b/gcc/testsuite/g++.dg/ipa/pr105438.C > new file mode 100644 > index 000000000000..45daece51abf > --- /dev/null > +++ b/gcc/testsuite/g++.dg/ipa/pr105438.C > @@ -0,0 +1,33 @@ > +// PR ipa/105438 > +// { dg-do compile } > +// { dg-options "-O2 -fno-strict-aliasing -fno-early-inlining -Warray-bounds > -fdump-ipa-icf-optimized" } > + > +// With -fno-strict-aliasing, IPA ICF merged configure<7> and configure<2>, > +// whose accesses through IN differ only in the bound of the array type, > +// and the merged body, inlined for the call with SHORTER, read it as a > +// const int[7]. (-fno-early-inlining keeps the instantiations from being > +// inlined into main before ICF runs.) > + > +int longer[7] = {}; > +int shorter[2] = {}; > +int out[10] = {}; > + > +template <int N> > +void configure(const int(&in)[N], const int nrows = N) > +{ > + if (nrows <= 10) > + { > + for (int i = 0; i < nrows; i++) > + { > + out[i] = in[i]; // { dg-bogus "outside array bounds" } > + } > + } > +} > + > +int main() > +{ > + configure(longer); > + configure(shorter); > +} > + > +// { dg-final { scan-ipa-dump-not "Semantic equality hit" "icf" } } > diff --git a/gcc/testsuite/gcc.dg/ipa/ipa-icf-41.c > b/gcc/testsuite/gcc.dg/ipa/ipa-icf-41.c > new file mode 100644 > index 000000000000..64bf871de0f8 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/ipa/ipa-icf-41.c > @@ -0,0 +1,73 @@ > +/* Verify that with -fno-strict-aliasing, IPA ICF doesn't merge functions > + whose memory accesses go through types of different sizes, which would > + put one function's types into the other's inlined copies, and that it > + still merges functions whose accesses go through the same types, or > + through distinct types of the same layout. > + { dg-do compile } > + { dg-options "-O2 -fno-strict-aliasing -fno-early-inlining -Warray-bounds > -fdump-ipa-icf-optimized" } */ > + > +struct small { char pad[16]; int a; unsigned int b, c; }; > +struct big { char pad[16]; int a; unsigned int b, c; int extra; }; > +struct small_twin { char pad[16]; int a; unsigned int b, c; }; > + > +/* Not merged: the same offsets, but through types of different sizes. > + Merged, the call to big_to_small in f would get small_to_big's body, > + which stores to TMP through a "struct big *". */ > + > +void small_to_big (struct big *dst, const struct small *src) > +{ > + dst->a = src->a; /* { dg-bogus "outside array bounds" } */ > + dst->b = src->b; /* { dg-bogus "outside array bounds" } */ > + dst->c = src->c; /* { dg-bogus "outside array bounds" } */ > +} > + > +void big_to_small (struct small *dst, const struct big *src) > +{ > + dst->a = src->a; > + dst->b = src->b; > + dst->c = src->c; > +} > + > +void f (struct big *p) > +{ > + struct small tmp; > + > + big_to_small (&tmp, p); > + small_to_big (p, &tmp); > +} > + > +/* Merged: the same types. */ > + > +void big_to_big_1 (struct big *dst, const struct big *src) > +{ > + dst->a = src->a; > + dst->b = src->b; > + dst->c = src->c; > +} > + > +void big_to_big_2 (struct big *dst, const struct big *src) > +{ > + dst->a = src->a; > + dst->b = src->b; > + dst->c = src->c; > +} > + > +/* Merged: distinct types of the same layout. */ > + > +void small_to_twin (struct small_twin *dst, const struct small *src) > +{ > + dst->a = src->a; > + dst->b = src->b; > + dst->c = src->c; > +} > + > +void twin_to_small (struct small *dst, const struct small_twin *src) > +{ > + dst->a = src->a; > + dst->b = src->b; > + dst->c = src->c; > +} > + > +/* { dg-final { scan-ipa-dump-not {Semantic equality > hit:[^\n]*(small_to_big|big_to_small)/} "icf" } } */ > +/* { dg-final { scan-ipa-dump {Semantic equality > hit:big_to_big_[12]/[0-9]+->big_to_big_[12]/[0-9]+} "icf" } } */ > +/* { dg-final { scan-ipa-dump {Semantic equality > hit:(small_to_twin|twin_to_small)/[0-9]+->(small_to_twin|twin_to_small)/[0-9]+} > "icf" } } */ > diff --git a/gcc/testsuite/gcc.dg/ipa/ipa-icf-42.c > b/gcc/testsuite/gcc.dg/ipa/ipa-icf-42.c > new file mode 100644 > index 000000000000..cb6a0ab9fdc3 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/ipa/ipa-icf-42.c > @@ -0,0 +1,75 @@ > +/* Verify that with -fno-strict-aliasing, IPA ICF compares each level of > + an access path, not just its base: an array element against a scalar > + member, a nested size mismatch, an array member that ends its object > + against one that doesn't, and a flexible array member against a > + zero-length one keep functions apart, while > + flexible array members of the same layout, and identical functions > + accessing variably modified types, still merge. > + { dg-do compile } > + { dg-options "-O2 -fno-strict-aliasing -fdump-ipa-icf-details" } */ > + > +/* ICF hashes memory accesses with their alias sets even with > + -fno-strict-aliasing, so functions accessing distinct structs would > + not get the same hash and would never be compared. may_alias gives > + each struct alias set 0, so each pair below is compared, which the > + "Equals called for" scans check, along with the reason for each > + refusal. */ > + > +#define MA __attribute__ ((may_alias)) > + > +/* Not merged: the stores are to the same offset and size, but one is to > + an array element and the other to a scalar member, so the access > + paths have different tree codes. (The tree code of a load is part of > + ICF's hash, so a load would not be compared.) */ > + > +struct MA c1 { int tag[2]; int x[1]; int y; }; > +struct MA c2 { int tag[2]; int x; int y; }; > + > +void codes_1 (struct c1 *dst, int v) { dst->x[0] = v; } > +void codes_2 (struct c2 *dst, int v) { dst->x = v; } > + > +/* Not merged: the outer structs are the same size, but the structs > + holding A are 16 and 8 bytes. */ > + > +struct MA n1 { struct { int a; int pad[3]; } in; int t[1]; }; > +struct MA n2 { struct { int a; int pad; } in; int t[3]; }; > + > +void nested_1 (struct n1 *dst, const struct n1 *src) { dst->in.a = > src->in.a; } > +void nested_2 (struct n2 *dst, const struct n2 *src) { dst->in.a = > src->in.a; } > + > +/* Not merged: both structs are 16 bytes, but ARR ends struct t1, so it > + may be a flexible array, and not struct t2. */ > + > +struct MA __attribute__ ((aligned (16))) t1 { int tag; int arr[1]; }; > +struct MA __attribute__ ((aligned (16))) t2 { int tag; int arr[1]; int x; }; > + > +void trailing_1 (struct t1 *dst, const struct t1 *src) { dst->arr[0] = > src->arr[0]; } > +void trailing_2 (struct t2 *dst, const struct t2 *src) { dst->arr[0] = > src->arr[0]; } > + > +/* Merged: flexible array members, whose types have no size. Not merged: > + either of those and a zero-length array. */ > + > +struct MA f1 { int tag; int arr[]; }; > +struct MA f2 { int tag; int arr[]; }; > +struct MA z { int tag; int arr[0]; }; > + > +void flex_1 (struct f1 *dst, const struct f1 *src, int i) { dst->arr[i] = > src->arr[i]; } > +void flex_2 (struct f2 *dst, const struct f2 *src, int i) { dst->arr[i] = > src->arr[i]; } > +void zero_len (struct z *dst, const struct z *src, int i) { dst->arr[i] = > src->arr[i]; } > + > +/* Merged: the sizes of variably modified types refer to each function's > + own parameters. */ > + > +void vla_1 (int n, int (*p)[n], int i) { p[i][1] = 1; p[i][2] = 2; } > +void vla_2 (int n, int (*p)[n], int i) { p[i][1] = 1; p[i][2] = 2; } > + > +/* Each refusal is logged as its reason, a few more lines, and then the > + pair compared; match the reason and the pair with no other comparison > + between them. */ > +/* { dg-final { scan-ipa-dump {access path difference\)'[^\n]*\n(?:(?!Equals > called)[^\n]*\n)*Equals called for: codes_[12]/[0-9]+:codes_[12]/[0-9]+ with > result: false} "icf" } } */ > +/* { dg-final { scan-ipa-dump {access path difference\)'[^\n]*\n(?:(?!Equals > called)[^\n]*\n)*Equals called for: nested_[12]/[0-9]+:nested_[12]/[0-9]+ > with result: false} "icf" } } */ > +/* { dg-final { scan-ipa-dump {access path difference\)'[^\n]*\n(?:(?!Equals > called)[^\n]*\n)*Equals called for: trailing_[12]/[0-9]+:trailing_[12]/[0-9]+ > with result: false} "icf" } } */ > +/* { dg-final { scan-ipa-dump {access path difference\)'[^\n]*\n(?:(?!Equals > called)[^\n]*\n)*Equals called for: > (zero_len/[0-9]+:flex_[12]|flex_[12]/[0-9]+:zero_len)/[0-9]+ with result: > false} "icf" } } */ > +/* { dg-final { scan-ipa-dump {Semantic equality > hit:flex_[12]/[0-9]+->flex_[12]/[0-9]+} "icf" } } */ > +/* { dg-final { scan-ipa-dump {Semantic equality > hit:vla_[12]/[0-9]+->vla_[12]/[0-9]+} "icf" } } */ > +/* { dg-final { scan-ipa-dump-not {Semantic equality > hit:[^\n]*(codes_|nested_|trailing_|zero_len)} "icf" } } */ > diff --git a/gcc/tree-ssa-alias.cc b/gcc/tree-ssa-alias.cc > index a8196d116355..2f3d7f523a02 100644 > --- a/gcc/tree-ssa-alias.cc > +++ b/gcc/tree-ssa-alias.cc > @@ -4262,7 +4262,9 @@ types_equal_for_same_type_for_tbaa_p (tree type1, tree > type2) > /* Compare REF1 and REF2 and return flags specifying their differences. > If LTO_STREAMING_SAFE is true do not use alias sets and canonical > types that are going to be recomputed. > - If TBAA is true also compare TBAA metadata. */ > + If TBAA is true also compare TBAA metadata, and otherwise only the > + shape of the access paths: the tree codes along them, the sizes of > + their types, and the kind of each array member. */ > > int > ao_compare::compare_ao_refs (ao_ref *ref1, ao_ref *ref2, > @@ -4379,8 +4381,36 @@ ao_compare::compare_ao_refs (ao_ref *ref1, ao_ref > *ref2, > || (MR_DEPENDENCE_BASE (rbase1) != MR_DEPENDENCE_BASE (rbase2)))) > flags |= DEPENDENCE_CLIQUE; > > + /* Without TBAA, the types along the access paths don't matter for > + aliasing, but other consumers read their sizes (e.g. ranger > + diagnostics like -Warray-bounds). Require the same tree codes at > + each level, with sizes that match and array members of the same > + kind, so the references of a merged function are valid for the > + callers of both. The types themselves may differ, so functions > + accessing distinct but identically laid out types still merge. */ > if (!tbaa) > - return flags; > + { > + tree c1 = ref1->ref, c2 = ref2->ref; > + while (true) > + { > + if (TREE_CODE (c1) != TREE_CODE (c2)) > + return flags | ACCESS_PATH; > + tree size1 = TYPE_SIZE (TREE_TYPE (c1)); > + tree size2 = TYPE_SIZE (TREE_TYPE (c2)); > + if (size1 && size2 > + ? !operand_equal_p (size1, size2, 0) > + : size1 != size2) > + return flags | ACCESS_PATH; > + if (TREE_CODE (c1) == COMPONENT_REF > + && component_ref_sam_type (c1) != component_ref_sam_type (c2)) > + return flags | ACCESS_PATH; > + if (!handled_component_p (c1)) > + break; > + c1 = TREE_OPERAND (c1, 0); > + c2 = TREE_OPERAND (c2, 0); > + } > + return flags; > + } > > /* Alias sets are not stable across LTO sreaming; be conservative here > and compare types the alias sets are ultimately based on. */ > -- > 2.55.0 >
