[v3] ld: Maintain the input file order

Message ID CAMe9rOqzyVvceVmw9b=73_Wxd-OOt500k7EKz7PDNAzbJyu-aw@mail.gmail.com
State New
Headers
Series [v3] ld: Maintain the input file order |

Commit Message

H.J. Lu April 22, 2026, 8:48 a.m. UTC
  On Wed, Apr 22, 2026 at 3:42 PM Jan Beulich <jbeulich@suse.com> wrote:
>
> On 22.04.2026 08:54, H.J. Lu wrote:
> > On Wed, Apr 22, 2026 at 2:17 PM Jan Beulich <jbeulich@suse.com> wrote:
> >> On 21.04.2026 23:12, H.J. Lu wrote:
> >>> On Wed, Apr 22, 2026 at 4:22 AM H.J. Lu <hjl.tools@gmail.com> wrote:
> >>>>
> >>>> When adding a new input archive, which comes from a linker script file
> >>>> and isn't referenced by any inputs, ld appends it to the input file list.
> >>>> On Linux, the input file order looks like
> >>>>
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
> >>>> x.o (symbol from plugin)
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
> >>>> /usr/lib64/libmvec.a
> >>>> /usr/lib64/libm-2.42.a
> >>>>
> >>>> since the compiler may not add compiler builtin functions to the LTO
> >>>> symbol table as it doesn't really know if builtin functions will have
> >>>> real symbols.  When ld extracts an element from the archive later during
> >>>> LTO rescan, the final input file order is
> >>>>
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
> >>>> x.o (symbol from plugin)
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
> >>>> fclrexcpt.o
> >>>>
> >>>> where x.o references the builtin function, feclearexcept which is defined
> >>>> in fclrexcpt.o from /usr/lib64/libm-2.42.a.
> >>>>
> >>>> As the result, the .eh_frame section terminator in crtn.o is placed before
> >>>> fclrexcpt.o and the .eh_frame section in the output isn't terminated.  The
> >>>> output crashes when it runs over the .eh_frame section during EH frame
> >>>> registration.  Insert the new input file after the current input file to
> >>>> maintain the input file order:
> >>>>
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
> >>>> x.o (symbol from plugin)
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
> >>>> /usr/lib64/libmvec.a
> >>>> /usr/lib64/libm-2.42.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
> >>>> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
> >>>>
> >>>> to properly terminate the .eh_frame section.
> >>>>
> >>>> PR ld/34088
> >>>> * ldlang.c (current_input_file): Changed to the pointer to
> >>>> lang_input_statement_type.
> >>>> (new_afile): Insert the new input file after the current input
> >>>> file to maintain the input file order.
> >>>> (lang_add_input_file): Updated.
> >>>> (load_symbols): Likewise.
> >>>> * testsuite/ld-plugin/lto.exp: Run PR ld/34088 test.
> >>>> * testsuite/ld-plugin/pr34088.c: New file.
> >>>>
> >>>> --
> >>>> H.J.
> >>>
> >>> Changes from v1:
> >>>
> >>> 1. Add debug_input_files.
> >>> 2. Insert the new input file before the current input file to keep
> >>> the order within the linker script file.
> >>
> >> Why would "before" vs "after" matter? Plus in the new code in new_afile()
> >
> > /usr/lib64/libm.a has
> >
> > GROUP ( /usr/lib64/libm-2.42.a /usr/lib64/libmvec.a )
> >
> > With before -lm, the input file order is:
> >
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
> > /usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
> > x.o (symbol from plugin)
> > /usr/lib64/libm-2.42.a
> > /usr/lib64/libmvec.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
> >
> > which is the same as x.o isn't an LTO input.   With after -lm,
> > the input file order is:
> >
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
> > /usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
> > x.o (symbol from plugin)
> > /usr/lib64/libmvec.a
> > /usr/lib64/libm-2.42.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
>
> That's still "before", just with the order of the two libs reversed,
> isn't it? I.e. don't you mean
>
> /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
> /usr/lib64/libmvec.a
> /usr/lib64/libm-2.42.a
>

Correct.

> ? But yes, I see how inserting before makes it easier to maintain the
> order as specified by the script.
>
> > /usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
> > /usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
> > /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
> >
> > "before" makes the input file order the same with and without
> > LTO input.
> >
> >> it very much looks as if prev still being NULL could be de-referenced.
> >> input_file_chain.head starts out as NULL, after all, and hence it could
> >
> > input_file_chain.head starts with a "null" item:
> >
> > (gdb) r `cat doit`
> > The program being debugged has been started already.
> > Start it from the beginning? (y or n) y
> > Starting program:
> > /export/build/gnu/tools-build/binutils-gitlab/build-x86_64-linux/ld/ld-new
> > `cat doit`
> > [Thread debugging using libthread_db enabled]
> > Using host libthread_db library "/lib64/libthread_db.so.1".
> >
> > Breakpoint 1, lang_add_input_file (name=0x0,
> >     file_type=lang_input_file_is_marker_enum, target=0x0)
> >     at /export/gnu/import/git/gitlab/x86-binutils/ld/ldlang.c:1323
> > 1323   if (name != NULL
> > (gdb) call debug_input_files ()
> > (gdb) c
> > Continuing.
> >
> > Breakpoint 1, lang_add_input_file (
> >     name=0x7fffffffddba
> > "/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o",
> > file_type=lang_input_file_is_file_enum, target=0x0)
> >     at /export/gnu/import/git/gitlab/x86-binutils/ld/ldlang.c:1323
> > 1323   if (name != NULL
> > (gdb) call debug_input_files ()
> > input: (null)
> > (gdb)
> >
> > "prev" will never be NULL.
>
> Alright, but see how non-obvious this is? If the first item in the list
> is a null one, why not (keeping the bogus cast; see below) e.g.
>
>       for (prev = (void *) input_file_chain.head, f = prev->next_real_file;

Fixed with

      prev = &input_file_chain.head->input_statement;
      for (f = prev->next_real_file;
           f != NULL;
           f = f->next_real_file)

>            f != NULL;
>            f = f->next_real_file)
>
> making entirely obvious that with a non-empty list (as guaranteed by
> current_input_file being non-NULL and needing to be on the list) prev
> won't be NULL? Plus this is reducing the loop by one iteration, albeit
> at the expense of marginally higher loop setup cost.
>
> >> very well point at current_input_file.
> >>
> >> The casting to void * there also isn't nice. Can't this be
> >> &input_file_chain.head->input_statement? Same in debug_input_files().
> >> There f also wants to be pointer-to-const.
> >
> > This is how the current input_file_chain.head is referenced in all other places.
> > I'd like to keep it this way for consistency.
>
> Well, yes, that's the one side of it. The other is that some time ago
> we started to try to limit the number of casts, because quite a few
> actually are bogus, risky, and/or UB. Imo in new code we ought to do
> better. And ideally we'd switch over existing code.

Fixed with

f = &input_file_chain.head->input_statement;

> >> As to the new testcase: Doesn't that make assumptions on how runtime
> >> libraries are structured on the target? IOW is the test passing really
> >> an indication of the issue at hand not occurring?
> >
> > It is true that such a test won't fail without libm.a being a linker script
> > if the bug isn't fixed.   But it will fail on glibc targets.  Its coverage
> > should be sufficient.
>
> Can this restriction at least be made explicit in the description,
> please? (Also even if libm.a is a linker script, the issue may still
> not be covered. So the wording in the description will want to be yet
> more general.)
>

I added

Add a static LTO test to reference feclearexcept which is a compiler
builtin function and isn't in the LTO symbol table when GCC is used.
It triggers the run-time crash on glibc targets of a linker script
libm.a without this fix when GCC 13 or above is used:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124869

in the commit message.

Here is the v3 patch.
  

Comments

Jan Beulich April 22, 2026, 12:01 p.m. UTC | #1
On 22.04.2026 10:48, H.J. Lu wrote:
> I added
> 
> Add a static LTO test to reference feclearexcept which is a compiler
> builtin function and isn't in the LTO symbol table when GCC is used.
> It triggers the run-time crash on glibc targets of a linker script
> libm.a without this fix when GCC 13 or above is used:
> 
> https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124869
> 
> in the commit message.

And that behavior is independent of glibc version (and other factors
which may influence its behavior)?

> Here is the v3 patch.

Okay.

Jan
  
H.J. Lu April 22, 2026, 10:04 p.m. UTC | #2
On Wed, Apr 22, 2026 at 8:01 PM Jan Beulich <jbeulich@suse.com> wrote:
>
> On 22.04.2026 10:48, H.J. Lu wrote:
> > I added
> >
> > Add a static LTO test to reference feclearexcept which is a compiler
> > builtin function and isn't in the LTO symbol table when GCC is used.
> > It triggers the run-time crash on glibc targets of a linker script
> > libm.a without this fix when GCC 13 or above is used:
> >
> > https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124869
> >
> > in the commit message.
>
> And that behavior is independent of glibc version (and other factors
> which may influence its behavior)?

The libm.a linker script was added to glibc 2.22 for x86-64 and to glibc
2.38 for aarch64.

> > Here is the v3 patch.

Pushed.

> Okay.
>
> Jan
  
Alan Modra April 22, 2026, 11:17 p.m. UTC | #3
The linker traverses the linker script for different purposes using
three pointers in a lang_input_statement_type: next_real_file, next
and header.next.  I suspect you may find this patch is incorrect due
to not reordering the header.next list.  See insert_input_file.
  
Alan Modra April 23, 2026, 1:02 a.m. UTC | #4
The test relies on having a glibc installed with a script libm.a.  As
far as I can see that means the test is only useful on x68_64 and
aarch64 with a newish glibc.  Please limit it, or write a more
generally useful test.

These fails turned up:
microblaze-linux-gnu  +FAIL: PR ld/34088
tilepro-linux-gnu  +FAIL: PR ld/34088

Both with "warning: feclearexcept is not implemented and will always
fail".
  
H.J. Lu April 23, 2026, 1:04 a.m. UTC | #5
On Thu, Apr 23, 2026 at 7:17 AM Alan Modra <amodra@gmail.com> wrote:
>
> The linker traverses the linker script for different purposes using
> three pointers in a lang_input_statement_type: next_real_file, next
> and header.next.  I suspect you may find this patch is incorrect due
> to not reordering the header.next list.  See insert_input_file.
>

There are more than one lang_statement_list.  statement_list is
traversed by header.next.   input_file_chain is traversed by
next_real_file for the real input file order.   For nodes on
input_file_chain, its header.next, next and next_real_file can
be different.   Before my patch, we have

(gdb) p *current_input_file
$28 = {header = {next = 0x6f41b0, type = lang_input_statement_enum},
  filename = 0x741db0
"/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a",
local_sym_name = 0x6f41a0 "-lm", sort_key = 0x0,
  extra_search_path = 0x0, the_bfd = 0x0, the_ctf = 0x0,
  section_flag_list = 0x0, next = 0x0, next_real_file = 0x6f41d0,
  target = 0x469b50 "elf64-x86-64", flags = {maybe_archive = 1,
    full_name_provided = 0, search_dirs = 0, sysrooted = 0, just_syms = 0,
    dynamic = 0, add_DT_NEEDED_for_dynamic = 0, add_DT_NEEDED_for_regular = 0,
    whole_archive = 0, link_mapless = 1, fake_archive = 0, loaded = 0,
    member = 0, real = 1, missing_file = 0, reload = 0, claimed = 0,
    claim_archive = 0, lto_output = 0, pushed = 0x0}}
(gdb) p *p
$29 = {header = {next = 0x0, type = lang_input_statement_enum},
  filename = 0x715770 "/usr/lib64/libm-2.42.a",
  local_sym_name = 0x715770 "/usr/lib64/libm-2.42.a", sort_key = 0x0,
  extra_search_path = 0x0, the_bfd = 0x0, the_ctf = 0x0,
  section_flag_list = 0x0, next = 0x0, next_real_file = 0x0, target = 0x0,
  flags = {maybe_archive = 0, full_name_provided = 0, search_dirs = 1,
    sysrooted = 0, just_syms = 0, dynamic = 0, add_DT_NEEDED_for_dynamic = 0,
    add_DT_NEEDED_for_regular = 0, whole_archive = 0, link_mapless = 1,
    fake_archive = 0, loaded = 0, member = 0, real = 1, missing_file = 0,
    reload = 0, claimed = 0, claim_archive = 0, lto_output = 0, pushed = 0x0}}
(gdb)

My patch only changes the real input file order, not the other
traversing orders:

(gdb) p *current_input_file
$30 = {header = {next = 0x6f41b0, type = lang_input_statement_enum},
  filename = 0x741db0
"/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a",
local_sym_name = 0x6f41a0 "-lm", sort_key = 0x0,
  extra_search_path = 0x0, the_bfd = 0x0, the_ctf = 0x0,
  section_flag_list = 0x0, next = 0x0, next_real_file = 0x6f41d0,
  target = 0x469b50 "elf64-x86-64", flags = {maybe_archive = 1,
    full_name_provided = 0, search_dirs = 0, sysrooted = 0, just_syms = 0,
    dynamic = 0, add_DT_NEEDED_for_dynamic = 0, add_DT_NEEDED_for_regular = 0,
    whole_archive = 0, link_mapless = 1, fake_archive = 0, loaded = 0,
    member = 0, real = 1, missing_file = 0, reload = 0, claimed = 0,
    claim_archive = 0, lto_output = 0, pushed = 0x0}}
(gdb) p *p
$31 = {header = {next = 0x0, type = lang_input_statement_enum},
  filename = 0x715770 "/usr/lib64/libm-2.42.a",
  local_sym_name = 0x715770 "/usr/lib64/libm-2.42.a", sort_key = 0x0,
  extra_search_path = 0x0, the_bfd = 0x0, the_ctf = 0x0,
  section_flag_list = 0x0, next = 0x0, next_real_file = 0x6f4130,
  target = 0x0, flags = {maybe_archive = 0, full_name_provided = 0,
    search_dirs = 1, sysrooted = 0, just_syms = 0, dynamic = 0,
    add_DT_NEEDED_for_dynamic = 0, add_DT_NEEDED_for_regular = 0,
    whole_archive = 0, link_mapless = 1, fake_archive = 0, loaded = 0,
    member = 0, real = 1, missing_file = 0, reload = 0, claimed = 0,
    claim_archive = 0, lto_output = 0, pushed = 0x0}}
(gdb)

Do you have a test to show my patch is wrong?

Thanks.
  
Alan Modra April 23, 2026, 2:33 a.m. UTC | #6
On Thu, Apr 23, 2026 at 09:04:43AM +0800, H.J. Lu wrote:
> My patch only changes the real input file order, not the other
> traversing orders:

Yes, that was what I was flagging as a possible defect.  I don't think
lang_input_statement_type.next is a problem, but header.next may well
be.  Many places traverse that list.

> Do you have a test to show my patch is wrong?

No.  I didn't even try to analyse where things may go wrong, sorry.

Hmm, if you generate a linker map file you can see that although libm
appears before libc on the linker command file, libc object sections
appear before libm object sections.  I haven't checked, but you might
like to verify that library dependencies work.  ie. in this case that
an object in libm-2.42.a with a dependency on a libc.a object will
cause the libc.a object to be extracted.
  
H.J. Lu April 23, 2026, 2:39 a.m. UTC | #7
On Thu, Apr 23, 2026 at 10:33 AM Alan Modra <amodra@gmail.com> wrote:
>
> On Thu, Apr 23, 2026 at 09:04:43AM +0800, H.J. Lu wrote:
> > My patch only changes the real input file order, not the other
> > traversing orders:
>
> Yes, that was what I was flagging as a possible defect.  I don't think
> lang_input_statement_type.next is a problem, but header.next may well
> be.  Many places traverse that list.

Without my change in new_afile, before new_afile returns:

1315     lang_statement_append (&input_file_chain, p, &p->next_real_file);

               ^^^^^^^^^^^^^ Only next_real_file is chained.
(gdb) p *p
$4 = {header = {next = 0x0, type = lang_input_statement_enum},
                     ^^^^^^^^ header.next  == NULL
  filename = 0x715770 "/usr/lib64/libm-2.42.a",
  local_sym_name = 0x715770 "/usr/lib64/libm-2.42.a", sort_key = 0x0,
  extra_search_path = 0x0, the_bfd = 0x0, the_ctf = 0x0,
  section_flag_list = 0x0, next = 0x0, next_real_file = 0x0, target = 0x0,
                                       ^^^^^^^^^ next == NULL
  flags = {maybe_archive = 0, full_name_provided = 0, search_dirs = 1,
    sysrooted = 0, just_syms = 0, dynamic = 0, add_DT_NEEDED_for_dynamic = 0,
    add_DT_NEEDED_for_regular = 0, whole_archive = 0, link_mapless = 1,
    fake_archive = 0, loaded = 0, member = 0, real = 1, missing_file = 0,
    reload = 0, claimed = 0, claim_archive = 0, lto_output = 0, pushed = 0x0}}
(gdb)

> > Do you have a test to show my patch is wrong?
>
> No.  I didn't even try to analyse where things may go wrong, sorry.
>
> Hmm, if you generate a linker map file you can see that although libm
> appears before libc on the linker command file, libc object sections
> appear before libm object sections.  I haven't checked, but you might
> like to verify that library dependencies work.  ie. in this case that
> an object in libm-2.42.a with a dependency on a libc.a object will
> cause the libc.a object to be extracted.
>

I will check.
  
H.J. Lu April 23, 2026, 3:09 a.m. UTC | #8
On Thu, Apr 23, 2026 at 10:39 AM H.J. Lu <hjl.tools@gmail.com> wrote:
>
> On Thu, Apr 23, 2026 at 10:33 AM Alan Modra <amodra@gmail.com> wrote:
> >
> > On Thu, Apr 23, 2026 at 09:04:43AM +0800, H.J. Lu wrote:
> > > My patch only changes the real input file order, not the other
> > > traversing orders:
> >
> > Yes, that was what I was flagging as a possible defect.  I don't think
> > lang_input_statement_type.next is a problem, but header.next may well
> > be.  Many places traverse that list.
>
> Without my change in new_afile, before new_afile returns:
>
> 1315     lang_statement_append (&input_file_chain, p, &p->next_real_file);
>
>                ^^^^^^^^^^^^^ Only next_real_file is chained.
> (gdb) p *p
> $4 = {header = {next = 0x0, type = lang_input_statement_enum},
>                      ^^^^^^^^ header.next  == NULL
>   filename = 0x715770 "/usr/lib64/libm-2.42.a",
>   local_sym_name = 0x715770 "/usr/lib64/libm-2.42.a", sort_key = 0x0,
>   extra_search_path = 0x0, the_bfd = 0x0, the_ctf = 0x0,
>   section_flag_list = 0x0, next = 0x0, next_real_file = 0x0, target = 0x0,
>                                        ^^^^^^^^^ next == NULL
>   flags = {maybe_archive = 0, full_name_provided = 0, search_dirs = 1,
>     sysrooted = 0, just_syms = 0, dynamic = 0, add_DT_NEEDED_for_dynamic = 0,
>     add_DT_NEEDED_for_regular = 0, whole_archive = 0, link_mapless = 1,
>     fake_archive = 0, loaded = 0, member = 0, real = 1, missing_file = 0,
>     reload = 0, claimed = 0, claim_archive = 0, lto_output = 0, pushed = 0x0}}
> (gdb)
>
> > > Do you have a test to show my patch is wrong?
> >
> > No.  I didn't even try to analyse where things may go wrong, sorry.
> >
> > Hmm, if you generate a linker map file you can see that although libm
> > appears before libc on the linker command file, libc object sections
> > appear before libm object sections.  I haven't checked, but you might
> > like to verify that library dependencies work.  ie. in this case that
> > an object in libm-2.42.a with a dependency on a libc.a object will
> > cause the libc.a object to be extracted.
> >
>
> I will check.
>

It works correctly:

[hjl@gnu-tgl-3 pr34088]$ cat x.c
#include <fenv.h>

int
main (void)
{
  feclearexcept (FE_ALL_EXCEPT);
  return 0;
}
[hjl@gnu-tgl-3 pr34088]$ cat m.c
#include <stdio.h>

int
feclearexcept (int i)
{
  printf ("PASS: 0x%x\n", i);
}
[hjl@gnu-tgl-3 pr34088]$ make
gcc -B./ -flto   -c -o x.o x.c
gcc -B./ -O0   -c -o m.o m.c
ar -rv libdummy.a m.o
ar: creating libdummy.a
a - m.o
echo "GROUP (libdummy.a)" > libdummy-m.a
gcc -B./ -static -o x x.o libdummy-m.a
./x
PASS: 0x3d
rm m.o
[hjl@gnu-tgl-3 pr34088]$ gcc -B./ -static -o x x.o  libdummy-m.a
-Wl,-y,printf,-y,feclearexcept
/export/build/gnu/tools-build/binutils-gitlab/build-x86_64-linux/ld/ld-new:
/tmp/cc6Ghl8I.ltrans0.ltrans.o: reference to feclearexcept
/export/build/gnu/tools-build/binutils-gitlab/build-x86_64-linux/ld/ld-new:
./libdummy.a(m.o): definition of feclearexcept
/export/build/gnu/tools-build/binutils-gitlab/build-x86_64-linux/ld/ld-new:
./libdummy.a(m.o): reference to printf
/export/build/gnu/tools-build/binutils-gitlab/build-x86_64-linux/ld/ld-new:
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a(printf.o):
definition of printf
[hjl@gnu-tgl-3 pr34088]$
  

Patch

From 2ae32c1bf2b6bbbc1ab2827ff6bb4fe4ca234eba Mon Sep 17 00:00:00 2001
From: "H.J. Lu" <hjl.tools@gmail.com>
Date: Wed, 22 Apr 2026 03:53:20 +0800
Subject: [PATCH v3] ld: Maintain the input file order

When adding a new input archive, which comes from a linker script file
and isn't referenced by any inputs, ld appends it to the input file list.
On Linux, when -lm is used with /usr/lib64/libm.a:

GROUP ( /usr/lib64/libm-2.42.a /usr/lib64/libmvec.a )

the input file order looks like

/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
/usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
x.o (symbol from plugin)
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
/usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
/usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
/usr/lib64/libm-2.42.a
/usr/lib64/libmvec.a

since the compiler may not add compiler builtin functions to the LTO
symbol table as it doesn't really know if builtin functions will have
real symbols.  When ld extracts an element from the archive later during
LTO rescan, the final input file order is

/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
/usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
x.o (symbol from plugin)
/tmp/ccHN6O4n.ltrans0.ltrans.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
/usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
/usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
fclrexcpt.o

where x.o references the builtin function, feclearexcept which is defined
in fclrexcpt.o from /usr/lib64/libm-2.42.a.

As the result, the .eh_frame section terminator in crtn.o is placed before
fclrexcpt.o and the .eh_frame section in the output isn't terminated.  The
output crashes when it runs over the .eh_frame section during EH frame
registration.  Insert the new input file before the current input file to
maintain the same input file order:

/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
/usr/lib/gcc/x86_64-redhat-linux/15/crtbeginT.o
x.o (symbol from plugin)
/usr/lib64/libm-2.42.a
/usr/lib64/libmvec.a
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libm.a
/usr/lib/gcc/x86_64-redhat-linux/15/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/libc.a
/usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o

as the non-LTO input to properly terminate the .eh_frame section.

Add a static LTO test to reference feclearexcept which is a compiler
builtin function and isn't in the LTO symbol table when GCC is used.
It triggers the run-time crash on glibc targets of a linker script
libm.a without this fix when GCC 13 or above is used:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124869

Also add a debug function, debug_input_files, to display the input file
chain.  It is optimized out when compiler optimization is turned on.

	PR ld/34088
	* ldlang.c (current_input_file): Changed to the pointer to
	lang_input_statement_type.
	(new_afile): Insert the new input file before the current input
	file to maintain the input file order.
	(lang_add_input_file): Updated.
	(load_symbols): Likewise.
	(debug_input_files): New function.
	(lang_process): Reference it.
	* testsuite/ld-plugin/lto.exp: Run PR ld/34088 test.
	* testsuite/ld-plugin/pr34088.c: New file.

Signed-off-by: H.J. Lu <hjl.tools@gmail.com>
---
 ld/ldlang.c                      | 55 +++++++++++++++++++++++++++++---
 ld/testsuite/ld-plugin/lto.exp   | 10 ++++++
 ld/testsuite/ld-plugin/pr34088.c | 19 +++++++++++
 3 files changed, 79 insertions(+), 5 deletions(-)
 create mode 100644 ld/testsuite/ld-plugin/pr34088.c

diff --git a/ld/ldlang.c b/ld/ldlang.c
index 98adc90e58a..d75f9df4d43 100644
--- a/ld/ldlang.c
+++ b/ld/ldlang.c
@@ -135,7 +135,7 @@  lang_statement_list_type file_chain = { NULL, NULL };
    lang_input_statement_type statement (reached via input_statement field in a
    lang_statement_union).  */
 lang_statement_list_type input_file_chain;
-static const char *current_input_file;
+static lang_input_statement_type *current_input_file;
 struct bfd_elf_dynamic_list **current_dynamic_list_p;
 struct bfd_sym_chain entry_symbol = { NULL, NULL };
 const char *entry_section = ".text";
@@ -1287,7 +1287,32 @@  new_afile (const char *name,
       FAIL ();
     }
 
-  lang_statement_append (&input_file_chain, p, &p->next_real_file);
+  if (current_input_file != NULL)
+    {
+      lang_input_statement_type *f, *prev;
+
+      /* Insert the new input file before the current input file to
+	 maintain the input file order.  NB: The first item on the
+	 input file chain is a null one.  */
+      prev = &input_file_chain.head->input_statement;
+      for (f = prev->next_real_file;
+	   f != NULL;
+	   f = f->next_real_file)
+	{
+	  if (f == current_input_file)
+	    {
+	      p->next_real_file = prev->next_real_file;
+	      prev->next_real_file = p;
+	      break;
+	    }
+	  prev = f;
+	}
+
+      if (f == NULL)
+	abort ();
+    }
+  else
+    lang_statement_append (&input_file_chain, p, &p->next_real_file);
   return p;
 }
 
@@ -1319,7 +1344,10 @@  lang_add_input_file (const char *name,
       return ret;
     }
 
-  return new_afile (name, file_type, target, current_input_file);
+  return new_afile (name, file_type, target,
+		    (current_input_file
+		     ? current_input_file->filename
+		     : NULL));
 }
 
 struct out_section_hash_entry
@@ -3225,7 +3253,7 @@  load_symbols (lang_input_statement_type *entry,
 
       ldfile_assumed_script = true;
       parser_input = input_script;
-      current_input_file = entry->filename;
+      current_input_file = entry;
       yyparse ();
       current_input_file = NULL;
       ldfile_assumed_script = false;
@@ -7894,6 +7922,20 @@  lang_set_flags (lang_memory_region_type *ptr, const char *flags, int invert)
     }
 }
 
+static void
+debug_input_files (void)
+{
+  lang_input_statement_type *f;
+
+  for (f = &input_file_chain.head->input_statement;
+       f != NULL;
+       f = f->next_real_file)
+    if (f->the_bfd)
+      fprintf (stderr, "file: %s\n", f->the_bfd->filename);
+    else
+      fprintf (stderr, "input: %s\n", f->filename);
+}
+
 /* Call a function on each real input file.  This function will be
    called on an archive, but not on the elements.  */
 
@@ -8777,7 +8819,10 @@  lang_process (void)
   lang_common ();
 
   if (0)
-    debug_prefix_tree ();
+    {
+      debug_prefix_tree ();
+      debug_input_files ();
+    }
 
   resolve_wilds ();
 
diff --git a/ld/testsuite/ld-plugin/lto.exp b/ld/testsuite/ld-plugin/lto.exp
index 139bc0b9f98..62cca45f42b 100644
--- a/ld/testsuite/ld-plugin/lto.exp
+++ b/ld/testsuite/ld-plugin/lto.exp
@@ -929,6 +929,16 @@  set lto_run_elf_shared_tests [list \
    {-Wl,-R,tmpdir} {} \
    {pr31644a.c} {pr31644b.exe} {pass.out} {-flto} {c} {} \
    {-Wl,--as-needed tmpdir/pr31644b.a tmpdir/pr31644c.so}] \
+  [list {PR ld/34088} \
+   {-static -flto -fuse-linker-plugin} \
+   {} \
+   {pr34088.c} \
+   {pr34088.exe} \
+   {pass.out} \
+   {-flto -O0} \
+   {c} \
+   {} \
+   {-lm}] \
 ]
 
 # LTO run-time tests for ELF
diff --git a/ld/testsuite/ld-plugin/pr34088.c b/ld/testsuite/ld-plugin/pr34088.c
new file mode 100644
index 00000000000..dfa2ca6903b
--- /dev/null
+++ b/ld/testsuite/ld-plugin/pr34088.c
@@ -0,0 +1,19 @@ 
+/* A static LTO test to reference feclearexcept which is a compiler
+   builtin function and isn't in the LTO symbol table when GCC is used.
+   It triggers the run-time crash on glibc targets of a linker script
+   libm.a without the fix for PR ld/34088 when GCC 13 or above is used:
+
+   https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124869
+
+ */
+
+#include <stdio.h>
+#include <fenv.h>
+
+int
+main (void)
+{
+  feclearexcept (FE_ALL_EXCEPT);
+  printf ("PASS\n");
+  return 0;
+}
-- 
2.53.0