[1/3] PE-COFF: Fix weak external symbol resolution when strong undef is seen first

Message ID 20260530191522.57144-2-peter0x44@disroot.org
State New
Headers
Series PE-COFF: Fix weak external symbol resolution bugs |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_binutils_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_binutils_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_binutils_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_binutils_check--master-arm success Test passed

Commit Message

Peter Damianov May 30, 2026, 7:15 p.m. UTC
  When linking PE-COFF objects, a weak external symbol (C_NT_WEAK with an
aux record specifying a fallback alias) may fail to resolve if a strong
undefined reference to the same symbol is encountered before the weak
definition.  This causes "undefined reference" errors for symbols like
operator new or personality routines that GCC emits as weak externals
with a fallback to a default implementation.

There are two problems:

1. In coff_link_add_symbols, when the generic linker resolves a weak
   undefined against an existing strong undefined (NOACT in the action
   table), the COFF-specific symbol_class and aux record were not stored
   because the existing hash entry already had non-null class/type from
   the first (strong) object file.

2. In _bfd_coff_generic_relocate_section, the weak alias fallback only
   triggered for bfd_link_hash_undefweak symbols.  When a strong undef
   is seen first, the hash type stays bfd_link_hash_undefined (the
   generic linker does not downgrade it), so the fallback was skipped.

Fix by extending the condition in coff_link_add_symbols to also update
symbol_class and aux when the incoming symbol is a PE weak external
with aux and the existing hash is still undefined.  Also extend the
relocation handler to resolve the weak alias fallback for
bfd_link_hash_undefined symbols that carry C_NT_WEAK class and have
an aux record.

bfd/
        * cofflink.c (coff_link_add_symbols): Also store symbol_class
        and aux record when a PE weak external with aux meets an
        existing undefined hash entry.
        (_bfd_coff_generic_relocate_section): Also resolve weak alias
        fallback for undefined symbols with C_NT_WEAK class and aux.
---
 bfd/cofflink.c | 48 +++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 39 insertions(+), 9 deletions(-)
  

Comments

Alan Modra May 31, 2026, 10:24 p.m. UTC | #1
On Sat, May 30, 2026 at 03:15:20PM -0400, Peter Damianov wrote:
> When linking PE-COFF objects, a weak external symbol (C_NT_WEAK with an
> aux record specifying a fallback alias) may fail to resolve if a strong
> undefined reference to the same symbol is encountered before the weak
> definition.  This causes "undefined reference" errors for symbols like
> operator new or personality routines that GCC emits as weak externals
> with a fallback to a default implementation.
> 
> There are two problems:
> 
> 1. In coff_link_add_symbols, when the generic linker resolves a weak
>    undefined against an existing strong undefined (NOACT in the action
>    table), the COFF-specific symbol_class and aux record were not stored
>    because the existing hash entry already had non-null class/type from
>    the first (strong) object file.
> 
> 2. In _bfd_coff_generic_relocate_section, the weak alias fallback only
>    triggered for bfd_link_hash_undefweak symbols.  When a strong undef
>    is seen first, the hash type stays bfd_link_hash_undefined (the
>    generic linker does not downgrade it), so the fallback was skipped.
> 
> Fix by extending the condition in coff_link_add_symbols to also update
> symbol_class and aux when the incoming symbol is a PE weak external
> with aux and the existing hash is still undefined.  Also extend the
> relocation handler to resolve the weak alias fallback for
> bfd_link_hash_undefined symbols that carry C_NT_WEAK class and have
> an aux record.
> 
> bfd/
>         * cofflink.c (coff_link_add_symbols): Also store symbol_class
>         and aux record when a PE weak external with aux meets an
>         existing undefined hash entry.
>         (_bfd_coff_generic_relocate_section): Also resolve weak alias
>         fallback for undefined symbols with C_NT_WEAK class and aux.
> ---
>  bfd/cofflink.c | 48 +++++++++++++++++++++++++++++++++++++++---------
>  1 file changed, 39 insertions(+), 9 deletions(-)
> 
> diff --git a/bfd/cofflink.c b/bfd/cofflink.c
> index e5c8a987d69..a38f692a008 100644
> --- a/bfd/cofflink.c
> +++ b/bfd/cofflink.c
> @@ -477,13 +477,20 @@ coff_link_add_symbols (bfd *abfd,
>  	      /* If we don't have any symbol information currently in
>  		 the hash table, or if we are looking at a symbol
>  		 definition, then update the symbol class and type in
> -		 the hash table.  */
> +		 the hash table.  Also update if the incoming symbol is
> +		 a weak external with an aux record (PE COFF weak alias)
> +		 and the existing symbol is still undefined, so the
> +		 fallback alias information is preserved for the linker's
> +		 relocation resolution.  */
>  	      if (((*sym_hash)->symbol_class == C_NULL
>  		   && (*sym_hash)->type == T_NULL)
>  		  || sym.n_scnum != 0
>  		  || (sym.n_value != 0
>  		      && (*sym_hash)->root.type != bfd_link_hash_defined
> -		      && (*sym_hash)->root.type != bfd_link_hash_defweak))
> +		      && (*sym_hash)->root.type != bfd_link_hash_defweak)
> +		  || (IS_WEAK_EXTERNAL (abfd, sym)
> +		      && sym.n_numaux > 0
> +		      && (*sym_hash)->root.type == bfd_link_hash_undefined))

Should you be handling bfd_link_hash_undefweak here too?

>  		{
>  		  (*sym_hash)->symbol_class = sym.n_sclass;
>  		  if (sym.n_type != T_NULL)
> @@ -3065,14 +3072,24 @@ _bfd_coff_generic_relocate_section (bfd *output_bfd,
>  		     + sec->output_offset);
>  	    }
>  
> -	  else if (h->root.type == bfd_link_hash_undefweak)
> +	  else if (h->root.type == bfd_link_hash_undefweak
> +		   || (h->root.type == bfd_link_hash_undefined
> +		       && h->symbol_class == C_NT_WEAK && h->numaux == 1))
>  	    {
> -	      if (h->symbol_class == C_NT_WEAK && h->numaux == 1)
> +	      /* Weak undefined symbol: either GNU weak (no aux record) or
> +		 PE COFF weak external (C_NT_WEAK with aux record).
> +		 Also handles strong undefined symbols that carry PE weak
> +		 external metadata (when strong undef is seen before weak def,
> +		 the hash type stays bfd_link_hash_undefined but we preserve
> +		 the weak external class and aux for later resolution).  */
> +
> +	      bool is_pe_weak = (h->symbol_class == C_NT_WEAK && h->numaux == 1);
> +
> +	      if (is_pe_weak)
>  		{
> -		  /* See _Microsoft Portable Executable and Common Object
> +		  /* PE COFF weak external: resolve via fallback alias.
> +		     See _Microsoft Portable Executable and Common Object
>  		     File Format Specification_, section 5.5.3.
> -		     Note that weak symbols without aux records are a GNU
> -		     extension.
>  		     FIXME: All weak externals are treated as having
>  		     characteristic IMAGE_WEAK_EXTERN_SEARCH_NOLIBRARY (1).
>  		     These behave as per SVR4 ABI:  A library member
> @@ -3081,24 +3098,37 @@ _bfd_coff_generic_relocate_section (bfd *output_bfd,
>  		     See also linker.c: generic_link_check_archive_element. */
>  		  struct coff_link_hash_entry *h2 = NULL;
>  		  unsigned long symndx2 = h->aux->x_sym.x_tagndx.u32;
> +
>  		  if (symndx2 < obj_raw_syment_count (h->auxbfd))
>  		    h2 = obj_coff_sym_hashes (h->auxbfd)[symndx2];
>  
>  		  if (!h2 || h2->root.type == bfd_link_hash_undefined)
>  		    {
> +		      /* Fallback alias not found or still undefined.
> +			 Resolve to NULL.  */
>  		      sec = bfd_abs_section_ptr;
>  		      val = 0;
>  		    }
>  		  else
>  		    {
> +		      /* Use fallback alias target.  */
>  		      sec = h2->root.u.def.section;
>  		      val = h2->root.u.def.value
>  			+ sec->output_section->vma + sec->output_offset;
>  		    }
>  		}
>  	      else
> -		/* This is a GNU extension.  */
> -		val = 0;
> +		{
> +		  /* GNU extension: ELF-style weak symbol in COFF without
> +		     PE weak external aux record.  COFF has no native support
> +		     for weak symbols (unlike ELF where they're part of the
> +		     format).  PE COFF adds them via C_NT_WEAK storage class
> +		     with an aux record pointing to a fallback symbol.  GNU ld
> +		     extends this by allowing __attribute__((weak)) in COFF
> +		     objects even without the PE aux structure, treating them
> +		     like ELF weak symbols: resolve to NULL if not defined.  */
> +		  val = 0;
> +		}
>  	    }
>  
>  	  else if (! bfd_link_relocatable (info))
> -- 
> 2.54.0
  
Jan Beulich June 2, 2026, 5:31 a.m. UTC | #2
On 30.05.2026 21:15, Peter Damianov wrote:
> When linking PE-COFF objects, a weak external symbol (C_NT_WEAK with an
> aux record specifying a fallback alias) may fail to resolve if a strong
> undefined reference to the same symbol is encountered before the weak
> definition.  This causes "undefined reference" errors for symbols like
> operator new or personality routines that GCC emits as weak externals
> with a fallback to a default implementation.
> 
> There are two problems:
> 
> 1. In coff_link_add_symbols, when the generic linker resolves a weak
>    undefined against an existing strong undefined (NOACT in the action
>    table), the COFF-specific symbol_class and aux record were not stored
>    because the existing hash entry already had non-null class/type from
>    the first (strong) object file.
> 
> 2. In _bfd_coff_generic_relocate_section, the weak alias fallback only
>    triggered for bfd_link_hash_undefweak symbols.  When a strong undef
>    is seen first, the hash type stays bfd_link_hash_undefined (the
>    generic linker does not downgrade it), so the fallback was skipped.

For both of these, where is it written down what the required behavior
is? From your description it sounds as if you're moving from one extreme
("strong" as the overall result) to the other ("weak" as the overall
result), when it kind of feels as if the type of reference may need to
be retained per incoming object file. (Or else "strong" being the
overall result would look to be more likely to be correct.)

Jan
  
Martin Storsjö June 2, 2026, 9:01 a.m. UTC | #3
On Tue, 2 Jun 2026, Jan Beulich wrote:

>> 1. In coff_link_add_symbols, when the generic linker resolves a weak
>>    undefined against an existing strong undefined (NOACT in the action
>>    table), the COFF-specific symbol_class and aux record were not stored
>>    because the existing hash entry already had non-null class/type from
>>    the first (strong) object file.
>>
>> 2. In _bfd_coff_generic_relocate_section, the weak alias fallback only
>>    triggered for bfd_link_hash_undefweak symbols.  When a strong undef
>>    is seen first, the hash type stays bfd_link_hash_undefined (the
>>    generic linker does not downgrade it), so the fallback was skipped.
>
> For both of these, where is it written down what the required behavior
> is? From your description it sounds as if you're moving from one extreme
> ("strong" as the overall result) to the other ("weak" as the overall
> result), when it kind of feels as if the type of reference may need to
> be retained per incoming object file. (Or else "strong" being the
> overall result would look to be more likely to be correct.)

This stems from how the COFF weak external symbols work - they are quite 
different from ELF weak symbols - but with these changes, they can provide 
mostly the same user facing behaviour.

In COFF, a "weak external" symbol is an undefined symbol (section number 
0, which means undefined symbol, but storage class 
IMAGE_SYM_CLASS_WEAK_EXTERNAL instead of IMAGE_SYM_CLASS_EXTERNAL), which 
contains an AUX symbol entry, pointing at another symbol (which may be 
defined in the same object file, or undefined, meant to be resolved in 
another object file). If there is no other definition available of that 
symbol, the linker should use the symbol pointed to by the weak external 
instead.

This mechanism works for both cases of weak symbols:

- For a weak declaration (where we reference a symbol which may or may not 
exist elsewhere), we have a weak external pointing at a null symbol. So if 
nothing else provides a definition of it, we don't fail the link with an 
undefined symbol, but we instead redirect uses of it towards the null 
symbol. This works with uses like this:

void __attribute__((weak)) maybe_exists(void);

void call(void) {
     if (maybe_exists)
         maybe_exists();
}

- For a weak definition, we have the COFF weak external point at the 
concrete symbol definition instead. If there's a strong definition of it 
elsewhere, that should be used, but if there is none, then the weak 
external, pointing at the fallback function implementation, should be used 
instead.


So if the linker already has seen a regular (strong) undefined symbol, and 
sees a weak external of the same symbol from another object file (which on 
the object file level is an "undefined weak external", with a reference to 
another symbol), that weak external serves as a potential definition of 
that symbol, which should be retained and used, in case there's no other 
(non-weak) definition of the symbol.

// Martin
  

Patch

diff --git a/bfd/cofflink.c b/bfd/cofflink.c
index e5c8a987d69..a38f692a008 100644
--- a/bfd/cofflink.c
+++ b/bfd/cofflink.c
@@ -477,13 +477,20 @@  coff_link_add_symbols (bfd *abfd,
 	      /* If we don't have any symbol information currently in
 		 the hash table, or if we are looking at a symbol
 		 definition, then update the symbol class and type in
-		 the hash table.  */
+		 the hash table.  Also update if the incoming symbol is
+		 a weak external with an aux record (PE COFF weak alias)
+		 and the existing symbol is still undefined, so the
+		 fallback alias information is preserved for the linker's
+		 relocation resolution.  */
 	      if (((*sym_hash)->symbol_class == C_NULL
 		   && (*sym_hash)->type == T_NULL)
 		  || sym.n_scnum != 0
 		  || (sym.n_value != 0
 		      && (*sym_hash)->root.type != bfd_link_hash_defined
-		      && (*sym_hash)->root.type != bfd_link_hash_defweak))
+		      && (*sym_hash)->root.type != bfd_link_hash_defweak)
+		  || (IS_WEAK_EXTERNAL (abfd, sym)
+		      && sym.n_numaux > 0
+		      && (*sym_hash)->root.type == bfd_link_hash_undefined))
 		{
 		  (*sym_hash)->symbol_class = sym.n_sclass;
 		  if (sym.n_type != T_NULL)
@@ -3065,14 +3072,24 @@  _bfd_coff_generic_relocate_section (bfd *output_bfd,
 		     + sec->output_offset);
 	    }
 
-	  else if (h->root.type == bfd_link_hash_undefweak)
+	  else if (h->root.type == bfd_link_hash_undefweak
+		   || (h->root.type == bfd_link_hash_undefined
+		       && h->symbol_class == C_NT_WEAK && h->numaux == 1))
 	    {
-	      if (h->symbol_class == C_NT_WEAK && h->numaux == 1)
+	      /* Weak undefined symbol: either GNU weak (no aux record) or
+		 PE COFF weak external (C_NT_WEAK with aux record).
+		 Also handles strong undefined symbols that carry PE weak
+		 external metadata (when strong undef is seen before weak def,
+		 the hash type stays bfd_link_hash_undefined but we preserve
+		 the weak external class and aux for later resolution).  */
+
+	      bool is_pe_weak = (h->symbol_class == C_NT_WEAK && h->numaux == 1);
+
+	      if (is_pe_weak)
 		{
-		  /* See _Microsoft Portable Executable and Common Object
+		  /* PE COFF weak external: resolve via fallback alias.
+		     See _Microsoft Portable Executable and Common Object
 		     File Format Specification_, section 5.5.3.
-		     Note that weak symbols without aux records are a GNU
-		     extension.
 		     FIXME: All weak externals are treated as having
 		     characteristic IMAGE_WEAK_EXTERN_SEARCH_NOLIBRARY (1).
 		     These behave as per SVR4 ABI:  A library member
@@ -3081,24 +3098,37 @@  _bfd_coff_generic_relocate_section (bfd *output_bfd,
 		     See also linker.c: generic_link_check_archive_element. */
 		  struct coff_link_hash_entry *h2 = NULL;
 		  unsigned long symndx2 = h->aux->x_sym.x_tagndx.u32;
+
 		  if (symndx2 < obj_raw_syment_count (h->auxbfd))
 		    h2 = obj_coff_sym_hashes (h->auxbfd)[symndx2];
 
 		  if (!h2 || h2->root.type == bfd_link_hash_undefined)
 		    {
+		      /* Fallback alias not found or still undefined.
+			 Resolve to NULL.  */
 		      sec = bfd_abs_section_ptr;
 		      val = 0;
 		    }
 		  else
 		    {
+		      /* Use fallback alias target.  */
 		      sec = h2->root.u.def.section;
 		      val = h2->root.u.def.value
 			+ sec->output_section->vma + sec->output_offset;
 		    }
 		}
 	      else
-		/* This is a GNU extension.  */
-		val = 0;
+		{
+		  /* GNU extension: ELF-style weak symbol in COFF without
+		     PE weak external aux record.  COFF has no native support
+		     for weak symbols (unlike ELF where they're part of the
+		     format).  PE COFF adds them via C_NT_WEAK storage class
+		     with an aux record pointing to a fallback symbol.  GNU ld
+		     extends this by allowing __attribute__((weak)) in COFF
+		     objects even without the PE aux structure, treating them
+		     like ELF weak symbols: resolve to NULL if not defined.  */
+		  val = 0;
+		}
 	    }
 
 	  else if (! bfd_link_relocatable (info))