On Thu, 15 Oct 2026, Richard Biener wrote:
> On Thu, 15 Oct 2026, Richard Biener wrote:
>
> > On Wed, 14 Oct 2026, Kees Cook wrote:
> >
> > > On Sun, Oct 11, 2026 at 04:50:41PM +0200, Richard Biener wrote:
> > > > > 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.
> > >
> > > The source has no such access, though: each function reads and writes
> > > members through its own types, and every byte touched is inside the
> > > object. The "struct big" access to a "struct small" exists only in the
> > > body ICF picked, so the warning is about code GCC wrote rather than
> > > code the programmer wrote.
> > >
> > > > So instead of this I'd rather work towards inlining the original
> > > > body rather than the ICF prevailing one.
> > >
> > > That does seem like it could be better, but it's not clear to me how
> > > large a project that would be to accomplish.
> >
> > I think for the case of ICF and non-LTO it should be possible to
> > "hack" this in; it should be just to make sure the inline edges
> > point to the original body and hopefully the ICF "merge" is somehow
> > encoded clearly. When the inline edge is set up correctly it should
> > possibly even work with LTO.
>
> I one-shotted the attached. It definitely fails to do this in a
> proper IPA way - it should have used IPA function/variable transform
> stages and stream out the "defered aliases" in
> write/read_optimization_summary.
>
> I'm sure this can be completed with some interaction, but our AI
> policy makes this questionable.
A few kg CO2 later here's a prototype that looks like proper IPA
and LTO capable. Note it still does varpool handling early which
I think is OK. I'm not sure it fits a possible design of different
inline from non-inline bodies in more general (GNU extern inline).
Thanks,
Richard.
> Richard.
>
> > > This patch seems to DTRT, though. It doesn't appear to regress merging
> > > in a meaningful way for the Linux at least.
> >
> > I don't dispute this, but it feels like a hack. Either ICF should
> > work as if -fstrict-aliasing were given, but consistently so, and
> > for a good reason, or you could as well do -fno-ipa-icf when
> > building the kernel as you are doing so many we-don't-quite-want-C
> > things already.
> >
> > > > And force-disable these kind of diagnostics with -fno-strict-aliasing?
> > >
> > > I'd really rather not. Linux builds everything with -fno-strict-aliasing,
> > > so that would remove these checks from the kernel entirely. GCC's
> > > -Warray-bounds is still off by default for the kernel because of false
> > > positives like this one (which is why I'm trying to solve it), and I've
> > > been working toward enabling it, since it finds real bugs. (I have a
> > > stack of a dozen real issues that this found while doing a recent pass.)
> > >
> > > For example (since I don't yet have links to the above stack), here
> > > are some random examples from a few years ago: a response struct passed
> > > where a bigger error struct was expected, whose "error" field was past
> > > the end of the object[1]:
> > >
> > > drivers/thunderbolt/xdomain.c:226:22: error: array subscript 'const
> > > struct tb_xdp_error_response[0]' is partly outside array bounds of
> > > 'struct tb_xdp_properties_changed_response[1]'
> > >
> > > and a u64 remainder stored into an int on the stack[2]:
> > >
> > > include/linux/math64.h:56:20: error: array subscript 'u64 {aka long
> > > long unsigned int}[0]' is partly outside array bounds of 'int[1]'
> > >
> > > But if you did disable diagnostics, which did you have in mind? If it's
> > > only warnings about an access whose type is bigger than the object while
> > > the bytes it touches are in bounds, that's narrower, but the first
> > > example above would still need to be caught, so I'd much rather just
> > > get ICF behaving nicely. :)
> > >
> > > -Kees
> > >
> > > [1] https://git.kernel.org/linus/19813551701d0
> > > [2] https://git.kernel.org/linus/9f3d543a9f737
> > >
> > >
> >
> >
>
>
--
Richard Biener <[email protected]>
SUSE Software Solutions Germany GmbH,
Frankenstrasse 146, 90461 Nuernberg, Germany;
GF: Stefan Gaiser, Jochen Jaser, Abhinav Puri; (HRB 36809, AG Nuernberg)From 4b2f31435c731fa2aa33863fda0fbcb8d6c5479a Mon Sep 17 00:00:00 2001
From: Richard Biener <[email protected]>
Date: Thu, 15 Oct 2026 12:58:25 +0200
Subject: [PATCH] ipa-icf: Defer creating aliases until after inlining
[PR105438]
When IPA-ICF merges two functions by creating an alias and releasing
the alias's body before inlining, inlining calls to the alias inlines
the ICF-chosen prevailing body instead of the original body. When the
functions operate on different types (such as different array bounds
or structure layouts), this mixes up types and declarations in the
inlined body, triggering bogus diagnostics such as -Warray-bounds.
This patch defers the creation of ICF symbol aliases and body removal
until after inlining. If an alias function was inlined into all of its
callers, its original body was inlined with its original types and
declarations and no alias needs to be created. If calls remain or the
function is needed offline, the alias to the prevailing function is
created after inlining.
In LTO mode, deferred aliases are streamed to LTRANS in the optimization
summary and ICF call redirections are applied via an IPA transform pass
so that LTO WHOPR partitions properly update call statements and create
symbol aliases in LTRANS units.
PR ipa/105438
gcc/ChangeLog:
* ipa-icf.h (ipa_icf_create_deferred_aliases): Declare.
* tree-pass.h (ipa_icf_create_deferred_aliases): Declare.
* ipa-icf.cc (deferred_merge_aliases): New static variable.
(sem_function::merge): Defer alias creation by pushing to
deferred_merge_aliases.
(ipa_icf_write_optimization_summary): New function.
(ipa_icf_read_optimization_summary): New function.
(ipa_icf_transform): New function.
(pass_ipa_icf::pass_ipa_icf): Register write_optimization_summary,
read_optimization_summary, and ipa_icf_transform hooks.
(ipa_icf_create_deferred_aliases): New function.
(ipa_icf_cc_finalize): Release deferred_merge_aliases.
* ipa-inline.cc (ipa_inline): Call ipa_icf_create_deferred_aliases.
gcc/testsuite/ChangeLog:
* g++.dg/ipa/pr105438.C: New test.
* g++.dg/ipa/pr105438-2.C: New test.
* g++.dg/lto/pr105438_0.C: New test.
---
gcc/ipa-icf.cc | 203 +++++++++++++++++++++++---
gcc/ipa-icf.h | 2 +
gcc/ipa-inline.cc | 5 +
gcc/testsuite/g++.dg/ipa/pr105438-2.C | 25 ++++
gcc/testsuite/g++.dg/ipa/pr105438.C | 25 ++++
gcc/testsuite/g++.dg/lto/pr105438_0.C | 26 ++++
gcc/tree-pass.h | 1 +
7 files changed, 267 insertions(+), 20 deletions(-)
create mode 100644 gcc/testsuite/g++.dg/ipa/pr105438-2.C
create mode 100644 gcc/testsuite/g++.dg/ipa/pr105438.C
create mode 100644 gcc/testsuite/g++.dg/lto/pr105438_0.C
diff --git a/gcc/ipa-icf.cc b/gcc/ipa-icf.cc
index 64cc722bd7a..8d580f99474 100644
--- a/gcc/ipa-icf.cc
+++ b/gcc/ipa-icf.cc
@@ -100,6 +100,7 @@ namespace ipa_icf {
are propagated later on. */
static sem_item_optimizer *optimizer = NULL;
+static vec<std::pair<cgraph_node *, cgraph_node *> > deferred_merge_aliases;
/* Constructor. */
@@ -1285,22 +1286,9 @@ sem_function::merge (sem_item *alias_item)
else if (create_alias)
{
alias->icf_merged = true;
+ original->icf_merged = true;
- /* Remove the function's body. */
- ipa_merge_profiles (original, alias);
- symtab->call_cgraph_removal_hooks (alias);
- alias->release_body (true);
- alias->reset ();
- /* Notice global symbol possibly produced RTL. */
- ((symtab_node *)alias)->call_for_symbol_and_aliases (clear_decl_rtl,
- NULL, true);
-
- /* Create the alias. */
- cgraph_node::create_alias (alias_func->decl, decl);
- alias->resolve_alias (original);
-
- original->call_for_symbol_thunks_and_aliases
- (set_local, (void *)(size_t) original->local_p (), true);
+ deferred_merge_aliases.safe_push (std::make_pair (original, alias));
if (dump_enabled_p ())
dump_printf (MSG_OPTIMIZED_LOCATIONS,
@@ -3677,6 +3665,126 @@ ipa_icf_driver (void)
return merged_p ? TODO_remove_functions : 0;
}
+/* Write optimization summary for IPA-ICF. */
+
+static void
+ipa_icf_write_optimization_summary (void)
+{
+ struct output_block *ob = create_output_block (LTO_section_ipa_icf);
+ lto_symtab_encoder_t encoder = ob->decl_state->symtab_node_encoder;
+ auto_vec<std::pair<cgraph_node *, cgraph_node *> > to_write;
+
+ for (unsigned i = 0; i < deferred_merge_aliases.length (); i++)
+ {
+ cgraph_node *original
+ = deferred_merge_aliases[i].first->ultimate_alias_target ();
+ cgraph_node *alias = deferred_merge_aliases[i].second;
+
+ if (lto_symtab_encoder_in_partition_p (encoder, alias)
+ || lto_symtab_encoder_in_partition_p (encoder, original)
+ || lto_symtab_encoder_lookup (encoder, alias) != LCC_NOT_FOUND)
+ to_write.safe_push (std::make_pair (original, alias));
+ }
+
+ streamer_write_uhwi (ob, to_write.length ());
+ for (unsigned i = 0; i < to_write.length (); i++)
+ {
+ cgraph_node *original = to_write[i].first;
+ cgraph_node *alias = to_write[i].second;
+
+ int orig_ref = lto_symtab_encoder_encode (encoder, original);
+ int alias_ref = lto_symtab_encoder_encode (encoder, alias);
+ streamer_write_uhwi (ob, orig_ref);
+ streamer_write_uhwi (ob, alias_ref);
+ }
+
+ produce_asm (ob);
+ destroy_output_block (ob);
+}
+
+/* Read optimization summary for IPA-ICF. */
+
+static void
+ipa_icf_read_optimization_summary (void)
+{
+ lto_file_decl_data **file_data_vec = lto_get_file_decl_data ();
+ lto_file_decl_data *file_data;
+ unsigned int j = 0;
+
+ while ((file_data = file_data_vec[j++]))
+ {
+ size_t len;
+ const char *data
+ = lto_get_summary_section_data (file_data, LTO_section_ipa_icf, &len);
+ if (!data)
+ continue;
+
+ const class lto_function_header *header
+ = (const struct lto_function_header *) data;
+ const int cfg_offset = sizeof (struct lto_function_header);
+ const int main_offset = cfg_offset + header->cfg_size;
+ const int string_offset = main_offset + header->main_size;
+ class data_in *data_in;
+ class lto_input_block ib_main ((const char *) data + main_offset, 0,
+ header->main_size, file_data);
+
+ data_in
+ = lto_data_in_create (file_data, (const char *) data + string_offset,
+ header->string_size, vNULL);
+
+ unsigned int count = streamer_read_uhwi (&ib_main);
+ lto_symtab_encoder_t encoder = file_data->symtab_node_encoder;
+
+ for (unsigned int i = 0; i < count; i++)
+ {
+ unsigned int orig_ref = streamer_read_uhwi (&ib_main);
+ unsigned int alias_ref = streamer_read_uhwi (&ib_main);
+
+ cgraph_node *original
+ = dyn_cast<cgraph_node *> (lto_symtab_encoder_deref (encoder,
+ orig_ref));
+ cgraph_node *alias
+ = dyn_cast<cgraph_node *> (lto_symtab_encoder_deref (encoder,
+ alias_ref));
+ if (original && alias)
+ {
+ std::pair<cgraph_node *, cgraph_node *> p (original, alias);
+ if (!deferred_merge_aliases.contains (p))
+ deferred_merge_aliases.safe_push (p);
+ }
+ }
+
+ lto_free_section_data (file_data, LTO_section_ipa_icf, NULL, data, len);
+ lto_data_in_delete (data_in);
+ }
+
+ if (deferred_merge_aliases.length ())
+ ::ipa_icf_create_deferred_aliases ();
+}
+
+/* Transform hook for IPA-ICF. */
+
+static unsigned int
+ipa_icf_transform (struct cgraph_node *node)
+{
+ bool modified = false;
+
+ for (cgraph_edge *e = node->callees; e; e = e->next_callee)
+ {
+ if (e->inline_failed
+ && e->callee->icf_merged
+ && e->call_stmt
+ && (!gimple_call_fndecl (e->call_stmt)
+ || e->callee->decl != gimple_call_fndecl (e->call_stmt)))
+ {
+ cgraph_edge::redirect_call_stmt_to_callee (e);
+ modified = true;
+ }
+ }
+
+ return modified ? TODO_update_ssa_only_virtuals : 0;
+}
+
const pass_data pass_data_ipa_icf =
{
IPA_PASS, /* type */
@@ -3698,13 +3806,11 @@ public:
ipa_icf_generate_summary, /* generate_summary */
ipa_icf_write_summary, /* write_summary */
ipa_icf_read_summary, /* read_summary */
- NULL, /*
- write_optimization_summary */
- NULL, /*
- read_optimization_summary */
+ ipa_icf_write_optimization_summary, /*
write_optimization_summary */
+ ipa_icf_read_optimization_summary, /*
read_optimization_summary */
NULL, /* stmt_fixup */
0, /* function_transform_todo_flags_start */
- NULL, /* function_transform */
+ ipa_icf_transform, /* function_transform */
NULL) /* variable_transform */
{}
@@ -3722,12 +3828,68 @@ public:
} // ipa_icf namespace
+using namespace ipa_icf;
+
ipa_opt_pass_d *
make_pass_ipa_icf (gcc::context *ctxt)
{
return new ipa_icf::pass_ipa_icf (ctxt);
}
+/* Create deferred ICF aliases after inlining. */
+
+void
+ipa_icf_create_deferred_aliases (void)
+{
+ for (unsigned i = 0; i < deferred_merge_aliases.length (); i++)
+ {
+ cgraph_node *original
+ = deferred_merge_aliases[i].first->ultimate_alias_target ();
+ cgraph_node *alias = deferred_merge_aliases[i].second;
+
+ if (alias->alias || alias->body_removed || alias->inlined_to)
+ continue;
+
+ /* If the alias function was inlined into all callers and can be
+ removed, its original body was inlined and no offline copy is
+ needed. */
+ if (!alias->callers && alias->can_remove_if_no_direct_calls_p ())
+ continue;
+
+ if (original->inlined_to)
+ continue;
+
+ if (alias->clones || (in_lto_p && original->in_other_partition))
+ {
+ /* If alias has inline clones, we cannot release its body.
+ Redirect direct callers to original instead. */
+ redirect_all_callers (alias, original);
+ continue;
+ }
+
+ cgraph_edge *e, *next;
+ for (e = alias->callees; e; e = next)
+ {
+ next = e->next_callee;
+ if (!e->inline_failed)
+ e->callee->remove_symbol_and_inline_clones ();
+ }
+
+ ipa_merge_profiles (original, alias);
+ symtab->call_cgraph_removal_hooks (alias);
+ alias->release_body (true);
+ alias->reset ();
+ ((symtab_node *)alias)->call_for_symbol_and_aliases (clear_decl_rtl,
+ NULL, true);
+ cgraph_node::create_alias (alias->decl, original->decl);
+ alias->resolve_alias (original);
+ original->call_for_symbol_thunks_and_aliases
+ (set_local, (void *)(size_t) original->local_p (), true);
+ }
+ if (!flag_wpa)
+ deferred_merge_aliases.release ();
+}
+
/* Reset all state within ipa-icf.cc so that we can rerun the compiler
within the same process. For use by toplev::finalize. */
@@ -3735,4 +3897,5 @@ void
ipa_icf_cc_finalize (void)
{
ipa_icf::optimizer = NULL;
+ deferred_merge_aliases.release ();
}
diff --git a/gcc/ipa-icf.h b/gcc/ipa-icf.h
index 4135ad3eb9c..daaa15e9d55 100644
--- a/gcc/ipa-icf.h
+++ b/gcc/ipa-icf.h
@@ -673,3 +673,5 @@ private:
}; // class sem_item_optimizer
} // ipa_icf namespace
+
+void ipa_icf_create_deferred_aliases (void);
diff --git a/gcc/ipa-inline.cc b/gcc/ipa-inline.cc
index ba8bf11b66e..767068e85ea 100644
--- a/gcc/ipa-inline.cc
+++ b/gcc/ipa-inline.cc
@@ -124,6 +124,8 @@ along with GCC; see the file COPYING3. If not see
#include "ipa-modref-tree.h"
#include "ipa-modref.h"
+extern void ipa_icf_create_deferred_aliases (void);
+
/* Inliner uses greedy algorithm to inline calls in a priority order.
Badness is used as the key in a Fibonacci heap which roughly corresponds
to negation of benefit to cost ratios.
@@ -3004,6 +3006,9 @@ ipa_inline (void)
if (dump_file)
ipa_dump_fn_summaries (dump_file);
+
+ ipa_icf_create_deferred_aliases ();
+
return remove_functions ? TODO_remove_functions : 0;
}
diff --git a/gcc/testsuite/g++.dg/ipa/pr105438-2.C
b/gcc/testsuite/g++.dg/ipa/pr105438-2.C
new file mode 100644
index 00000000000..64aa30c588a
--- /dev/null
+++ b/gcc/testsuite/g++.dg/ipa/pr105438-2.C
@@ -0,0 +1,25 @@
+// PR ipa/105438
+// { dg-do compile }
+// { dg-options "-O2 -fno-strict-aliasing -fno-early-inlining -Warray-bounds" }
+
+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);
+}
diff --git a/gcc/testsuite/g++.dg/ipa/pr105438.C
b/gcc/testsuite/g++.dg/ipa/pr105438.C
new file mode 100644
index 00000000000..c84fcb67b9f
--- /dev/null
+++ b/gcc/testsuite/g++.dg/ipa/pr105438.C
@@ -0,0 +1,25 @@
+// PR ipa/105438
+// { dg-do compile }
+// { dg-options "-O2 -fno-strict-aliasing -Warray-bounds" }
+
+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);
+}
diff --git a/gcc/testsuite/g++.dg/lto/pr105438_0.C
b/gcc/testsuite/g++.dg/lto/pr105438_0.C
new file mode 100644
index 00000000000..46e3b458b5f
--- /dev/null
+++ b/gcc/testsuite/g++.dg/lto/pr105438_0.C
@@ -0,0 +1,26 @@
+// { dg-lto-do link }
+// { dg-require-effective-target lto_incremental }
+// { dg-lto-options { { -O2 -flto -fno-strict-aliasing -Werror=array-bounds }
} }
+// { dg-extra-ld-options "-r -nostdlib" }
+
+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];
+ }
+ }
+}
+
+int main()
+{
+ configure(longer);
+ configure(shorter);
+}
diff --git a/gcc/tree-pass.h b/gcc/tree-pass.h
index a3b35e009e0..bdd60f72d81 100644
--- a/gcc/tree-pass.h
+++ b/gcc/tree-pass.h
@@ -539,6 +539,7 @@ extern simple_ipa_opt_pass *make_pass_ipa_free_fn_summary
(gcc::context *ctxt);
extern ipa_opt_pass_d *make_pass_ipa_cp (gcc::context *ctxt);
extern ipa_opt_pass_d *make_pass_ipa_sra (gcc::context *ctxt);
extern ipa_opt_pass_d *make_pass_ipa_icf (gcc::context *ctxt);
+extern void ipa_icf_create_deferred_aliases (void);
extern ipa_opt_pass_d *make_pass_ipa_devirt (gcc::context *ctxt);
extern ipa_opt_pass_d *make_pass_ipa_odr (gcc::context *ctxt);
extern ipa_opt_pass_d *make_pass_ipa_reference (gcc::context *ctxt);
--
2.51.0