Bug 33577 - Symbols without version bound to Base version
Summary: Symbols without version bound to Base version
Status: REOPENED
Alias: None
Product: binutils
Classification: Unclassified
Component: ld (show other bugs)
Version: 2.46
: P2 normal
Target Milestone: 2.46
Assignee: H.J. Lu
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2025-10-24 07:42 UTC by Rainer Orth
Modified: 2026-01-24 07:04 UTC (History)
9 users (show)

See Also:
Host:
Target: *-*-solaris2*, *-*-linux*
Build:
Last reconfirmed:
Project(s) to access:
ssh public key:


Attachments
Initial testcase (623 bytes, patch)
2025-10-24 07:44 UTC, Rainer Orth
Details | Diff
A patch (753 bytes, patch)
2025-10-29 01:37 UTC, H.J. Lu
Details | Diff
An updated patch (1.98 KB, patch)
2025-10-30 12:02 UTC, H.J. Lu
Details | Diff
A testcase for VER_NDX_GLOBAL (815 bytes, application/octet-stream)
2025-11-13 22:13 UTC, H.J. Lu
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Rainer Orth 2025-10-24 07:42:20 UTC
While working on a mini-testsuite for the Solaris ABI symbols (cf.
ld/emultempl/solaris2.em), I noticed an inconsistency between GNU ld and Solaris
ld.  Consider the following testcase:

$ cat vers33.c
extern void show_undef_weak (void) __attribute__((weak));

void
foo (void)
{
  if (show_undef_weak)
    show_undef_weak ();
}
$ cat vers33.map
VERS_1 {
  global:
    foo;
  local:
    *;
};
$ gcc -fPIC -shared -o vers33.so vers33.c -Wl,--version-script vers33.map
$ objdump --dynamic-syms vers33.so|grep Base
0000000000000000  w   D  *UND*	0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000  w   D  *UND*	0000000000000000  Base        __gmon_start__
0000000000000000  w   D  *UND*	0000000000000000  Base        show_undef_weak
0000000000000000  w   D  *UND*	0000000000000000  Base        _ITM_registerTMCloneTable

Several undef weak symbols show up as bound to the Base version: show_undef_weak
from the testcase, as well as a couple of others from crtbeginS.o and others.

This differs from the Solaris ld (which I consider as a reference for symbol
versioning given that Sun invented this stuff) and makes no sense at all:
those weak undef symbols certainly aren't an exported interface of the shared
objects, thus have no reason at all to show up as bound to the Base version in
the version *definition* (.gnu.version_d).  One might argue that they could be
in .gnu.version_r, but even that's not completely clear.

I tried to determine where this happens, but got lost in the maze of elflink.c

I'm attaching an initial patch with a testcase for this.
Comment 1 Rainer Orth 2025-10-24 07:44:29 UTC
Created attachment 16435 [details]
Initial testcase

2025-10-23  Rainer Orth  <ro@CeBiTec.Uni-Bielefeld.DE>

	ld:
	PR ld/33577
	* testsuite/ld-elfvers/vers33.c: New test.
	* testsuite/ld-elfvers/vers33.dsym: Likewise.
	* testsuite/ld-elfvers/vers33.map: Likewise.
	* testsuite/ld-elfvers/vers33.ver: Likewise.
	* testsuite/ld-elfvers/vers.exp: Run it.
Comment 2 H.J. Lu 2025-10-24 21:34:33 UTC
Symbol version script only applies to definitions, not references,
since linker doesn't know where/if/how show_undef_weak is defined
at run-time.  It can be unversioned or have some other version.
Comment 3 Ali Bahrami 2025-10-26 22:45:12 UTC
Is it usual here to close bugs immediately, without a chance to discuss? I'd like to ask that it be reopened, and that the discussion continue.

No one claimed that linker scripts control references. They don't, but that's not the issue being reported. The issue is that this reference symbol is being added to a version *definition* section. If it was being added to a version needed section, that would be another matter. Here, the linker script doesn't control the version assigned to show_undef_weak, but it does serve to enable the versioning plumbing generally, and that seems to be involved.

Let's look closer, using gld on Solaris, because that's what I'm running, but I would expect that it will be the same on Linux. To provide a point of reference, I've added a call to printf() to the original source file:

    % cat  vers33.c
    #include <stdio.h>
    
    extern void show_undef_weak (void) __attribute__((weak));

    void
    foo (void)
    {
      if (show_undef_weak)
        show_undef_weak ();

      printf("done");
    }

Start with a .o:

    % gcc -fPIC -shared -c vers33.c

Linking it without the linker script, we get the expected result:

    % gld --shared /usr/lib/64/crti.o vers33.o /usr/lib/64/crtn.o -lc
    % elfdump -v a.out

    Version Needed Section:  .gnu.version_r
      index  file       version   
        [2]  libc.so.1  SUNW_0.7
    % pvs -os a.out
    a.out - libc.so.1 (SUNW_0.7): printf;

That's all reasonable: printf() has been assigned to a version needed section for libc, and otherwise, there's no versioning in play.

Now, see what happens when we add in the linker script:

    % ld --shared /usr/lib/64/crti.o vers33.o --version-script vers33.map /usr/lib/64/crtn.o -lc
    % elfdump -v a.out

    Version Definition Section:  .gnu.version_d
      index  version    dependency
        [1]  a.out                  [ BASE ]
        [2]  VERS_1     

    Version Needed Section:  .gnu.version_r
      index  file       version   
        [3]  libc.so.1  SUNW_0.7
    % pvs -os a.out
    a.out - libc.so.1 (SUNW_0.7): printf;
    a.out - a.out: _PROCEDURE_LINKAGE_TABLE_;
    a.out - a.out: _edata;
    a.out - a.out: _etext;
    a.out - a.out: _end;
    a.out - a.out: _DYNAMIC;
    a.out - a.out: show_undef_weak;
    a.out - a.out: _GLOBAL_OFFSET_TABLE_;
    a.out - VERS_1: foo;

Note that show_undef_weak has been assigned to the Version Definition Section, and not a Version Needed Section (like printf). Doesn't it seem like something is off here? I agree with your point that "linker doesn't know where/if/how show_undef_weak is defined at run-time", but it certainly does know that it wasn't defined by the object it is building (a.out). My guess is that this is an unhandled case that simply "fell through" to the base definition, and not really the intended outcome.

That leaves the question of where it should be assigned. As you said, "It can be unversioned or have some other version". However, in this case, it can only be unversioned, because the only mention of it is in the declaration:

    extern void show_undef_weak (void) __attribute__((weak));

For it to have some other version, that symbol would need to have been seen in a dependency (like printf is seen in libc), which would then cause a Version Needed Record to be created. Here, there is nothing, so unversioned would seem to be the only valid option.

???
Comment 4 H.J. Lu 2025-10-28 01:03:55 UTC
We may be talking different things.  For me, the BASE version of a symbol is
the symbol without the symbol version.  The BASE version in the '.gnu.version_d'
section:

Version definition section '.gnu.version_d' contains 2 entries:
 Addr: 0x00000000000011c0  Offset: 0x000011c0  Link: 9 (.dynstr)
  000000: Rev: 1  Flags: BASE  Index: 1  Cnt: 1  Name: libx.so
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: VERS_1

comes from the shared library name.  Is the BASE version from the shared library
name an issue on Solaris?
Comment 5 Ali Bahrami 2025-10-28 03:06:49 UTC
I don't think this is specifically a Solaris issue, but rather,
just one that was noticed on Solaris by virtue of comparing objects
from the two link-editors.

My understanding of versioning:

    - Versioned symbols defined by the object (definitions) are associated
      with a VERDEF record.

    - Versioned symbols that are defined externally by some other object
      (references) are associated with a VERNEED record.

    - Symbols that are not versioned are given version index 0. These can
      be either definitions, or external references.

    - The BASE version, which has index 1, is the version that holds the
      symbols defined by the object itself, that have not otherwise been 
      assigned to a different version, possibly with a linker script.
      By convention, the version is given the SONAME of the object. This
      is a VERDEF, so external references don't belong here.

    - Named versions, first VERDEF, and then VERNEED, are given indexes
      starting at 2.

So in this example, we have the following versions:

    % elfdump -v a.out

    Version Definition Section:  .gnu.version_d
      index  version    dependency
        [1]  a.out                  [ BASE ]
        [2]  VERS_1     

    Version Needed Section:  .gnu.version_r
      index  file       version   
        [3]  libc.so.1  SUNW_0.7

Or summarizing:

    [0] Not versioned
    [1] BASE definitions
    [2] Definitions assigned to version VERS_1 by the linker script
    [3] External Symbols identified as coming from version SUNW_0.7 in libc

The problem then, is that show_undef_weak is an external reference,
not a definition, so it should not be assigned to any VERDEF (BASE,
or otherwise). The valid options would be version index 0 (not versioned),
or the index of a VERNEED record.

As you pointed out earlier, we have no information that would identify
such a VERNEED. The source of this symbol, if it even exists, is
completely unknown to the link-edit, so it has no version, and version
index 0 is the appropriate assignment for it.

I believe that these details are the same on Solaris as on GNU. I also
don't think it's causing any known problems today, as the example is  
artificial, and the approach using a weak symbol is poor. It's just
something  that was noticed while testing.
Comment 6 H.J. Lu 2025-10-28 12:12:50 UTC
(In reply to Ali Bahrami from comment #5)
> I believe that these details are the same on Solaris as on GNU. I also
> don't think it's causing any known problems today, as the example is  
> artificial, and the approach using a weak symbol is poor. It's just
> something  that was noticed while testing.

As I said in comment #4, BASE is unrelated to weak symbol:

[hjl@gnu-tgl-3 tmp]$ cat x.c
void
foo (void)
{
}
[hjl@gnu-tgl-3 tmp]$ cat x.map
VERS_1 {
  global:
    foo;
  local:
    *;
};
[hjl@gnu-tgl-3 tmp]$ gcc -c -fPIC x.c
[hjl@gnu-tgl-3 tmp]$ ld -o libx.so -shared x.o --version-script=x.map
[hjl@gnu-tgl-3 tmp]$ readelf -V libx.so

Version symbols section '.gnu.version' contains 3 entries:
 Addr: 0x0000000000001084  Offset: 0x00001084  Link: 3 (.dynsym)
  000:   0 (*local*)       2 (VERS_1)        2 (VERS_1)     

Version definition section '.gnu.version_d' contains 2 entries:
 Addr: 0x0000000000001090  Offset: 0x00001090  Link: 4 (.dynstr)
  000000: Rev: 1  Flags: BASE  Index: 1  Cnt: 1  Name: libx.so
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: VERS_1
[hjl@gnu-tgl-3 tmp]$
Comment 7 Ali Bahrami 2025-10-28 15:04:39 UTC
(In reply to H.J. Lu from comment #6)
> (In reply to Ali Bahrami from comment #5)
> > I believe that these details are the same on Solaris as on GNU. I also
> > don't think it's causing any known problems today, as the example is  
> > artificial, and the approach using a weak symbol is poor. It's just
> > something  that was noticed while testing.
> 
> As I said in comment #4, BASE is unrelated to weak symbol:

That's right. Other than having been used to create this
particular example, the weak attribute is not the issue,
and has nothing to do with BASE.

As you've said at the start, and as I've mentioned at every
opportunity, the symbol show_undef_weak is an external *reference*,
and not a *definition*. REFerences should not be assigned to
a verDEF index, and so show_undef_weak should not be assigned to
BASE. It should be set to version index 0.
Comment 8 H.J. Lu 2025-10-28 23:20:07 UTC
(In reply to Ali Bahrami from comment #7)
> 
> As you've said at the start, and as I've mentioned at every
> opportunity, the symbol show_undef_weak is an external *reference*,
> and not a *definition*. REFerences should not be assigned to
> a verDEF index, and so show_undef_weak should not be assigned to
> BASE. It should be set to version index 0.

I didn't see it:

[hjl@gnu-tgl-3 pr33577]$ cat x.c
extern void show_undef_weak (void) __attribute__((weak));

void
foo (void)
{
  if (show_undef_weak)
    show_undef_weak ();
}
[hjl@gnu-tgl-3 pr33577]$ make
ld -shared --version-script=x.map -o libx.so x.o
readelf -s -D -W -V libx.so

Symbol table for image contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND show_undef_weak
     2: 0000000000000220    17 FUNC    GLOBAL DEFAULT    3 foo@@VERS_1
     3: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  ABS VERS_1

Version symbols section '.gnu.version' contains 4 entries:
 Addr: 0x00000000000010ac  Offset: 0x000010ac  Link: 5 (.dynsym)
  000:   0 (*local*)       1 (*global*)      2 (VERS_1)        2 (VERS_1)     

Version definition section '.gnu.version_d' contains 2 entries:
 Addr: 0x00000000000010b8  Offset: 0x000010b8  Link: 6 (.dynstr)
  000000: Rev: 1  Flags: BASE  Index: 1  Cnt: 1  Name: libx.so
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: VERS_1
[hjl@gnu-tgl-3 pr33577]$
Comment 9 Ali Bahrami 2025-10-29 00:00:26 UTC
Sorry, I'm not following what the previous note is pointing out.
Don't you need to use objdump, as in Rainer's original note, to
show the version index?

He showed:

    $ objdump --dynamic-syms vers33.so
...
    0000000000000000  w   D  *UND*	0000000000000000  Base        show_undef_weak
                                                          ^^^^

While we're arguing that the output should be:

    0000000000000000  w   D  *UND*  0000000000000000              show_undef_weak

???

Thanks,

- Ali
Comment 10 H.J. Lu 2025-10-29 01:30:54 UTC
Reopened
Comment 11 H.J. Lu 2025-10-29 01:37:13 UTC
Created attachment 16441 [details]
A patch

Try this.
Comment 12 Ali Bahrami 2025-10-29 02:21:44 UTC
Thank you, I appreciate it.

I happen to know from an offline conversation that Rainer is
away for a couple of weeks, but I'm sure he'll jump on testing
the patch when he returns (I'm just the analysis part of the team)
but I looked at the patch and it looks like the right thing to me.
There seems to be some repetition that maybe could be factored out,
but I'll leave that to you guys, and am not worried about it either
way.

Thanks Again.
Comment 13 H.J. Lu 2025-10-30 12:02:10 UTC
Created attachment 16442 [details]
An updated patch
Comment 14 Sourceware Commits 2025-11-03 07:09:14 UTC
The master branch has been updated by H.J. Lu <hjl@sourceware.org>:

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=f685e3953f9a38a41bbd0a597f9882870cee13d5

commit f685e3953f9a38a41bbd0a597f9882870cee13d5
Author: H.J. Lu <hjl.tools@gmail.com>
Date:   Wed Oct 29 09:49:57 2025 +0800

    elf: Don't set its DT_VERSYM entry for unversioned symbol
    
    1. Referenced symbol without '@' has no version.
    2. Defined symbol without the .symver directive has no version if there
    is no linker version script.
    
    Symbol without version shouldn't have the base version in its DT_VERSYM
    entry.  Instead, its DT_VERSYM entry should be all zero to indicate that
    the symbol doesn't have a version.
    
    NB: Symbol with the base version has a '@' suffix, like "foo@", defined
    with
    
    .symver hide_original_foo, foo@
    
    bfd/
    
            PR ld/33577
            * elflink.c (elf_link_output_extsym): Don't set its DT_VERSYM
            entry for the symbol without version.
    
    ld/
    
            PR ld/33577
            * ld-elfvers/vers16.dsym: Remove the "Base" version on symbols
            without version.
    
    Signed-off-by: H.J. Lu <hjl.tools@gmail.com>
Comment 15 H.J. Lu 2025-11-03 07:10:49 UTC
Fixed for 2.46.
Comment 16 Fangrui Song 2025-11-12 06:34:22 UTC
I think the old linker behavior, which kept undefined unversioned symbols at version 1, was more sensible.

This is because version 0 (VER_NDX_LOCAL) used to be exclusively for defined symbols, but now it can be applied to undefined ones.
This change breaks the simple rule that an absent symbol versioning section (like .gnu.version) meant everything was version 1.
Now, a symbol's default version depends on whether it's defined.

The fact that version index 1 in the .gnu.version_d section specifies the library name (SONAME) shouldn't be a reason to default undefined symbols to version 0.

Could the objdump output be updated to stop printing "BASE" for undefined symbols?
Comment 17 Ali Bahrami 2025-11-12 19:04:16 UTC
The old behavior was wrong, as was beaten to death in the
preceding discussion. Hence, not sensible. Undefined
symbols can't belong to a symbol definition version.

I do understand that having something that seemed to
be stable change can be annoying to deal with, but the
assumption that version 0 was only for definitions isn't
right. Version 0 is for completely unversioned symbols.
Comment 18 H.J. Lu 2025-11-12 22:47:15 UTC
(In reply to Ali Bahrami from comment #17)
> The old behavior was wrong, as was beaten to death in the
> preceding discussion. Hence, not sensible. Undefined
> symbols can't belong to a symbol definition version.
> 
> I do understand that having something that seemed to
> be stable change can be annoying to deal with, but the
> assumption that version 0 was only for definitions isn't
> right. Version 0 is for completely unversioned symbols.

I think that

VER_NDX_LOCAL (0): Symbol has local scope

may confuse people.
Comment 19 Rui Ueyama 2025-11-13 01:03:13 UTC
This change completely breaks compatibility with the current and all previous versions of the mold linker. Specifically, mold cannot link against shared libraries if they are linked with GNU ld after f685e3953f9a38a41bbd0a597f9882870cee13d5.

This appears to be an aesthetic change with no real user benefit, yet it already causes a major compatibility issue. It is probably not limited to mold, as GNU ld has been emitting the symbol table version the way it currently does probably since the very beginning. I don’t think it is worth taking the risk. It seems we are breaking user environments with no real reward.
Comment 20 Ali Bahrami 2025-11-13 01:29:45 UTC
Yes, I see the same text in the Solaris <sys/link.h>,
and I agree that it is confusing:

    /*                                                                              
     * Versym symbol index values.  Values greater than VER_NDX_GLOBAL              
     * and less then VER_NDX_LORESERVE associate symbols with user                  
     * specified version descriptors.                                               
     */
    #define VER_NDX_LOCAL       0       /* symbol is local */
    #define VER_NDX_GLOBAL      1       /* symbol is global and assigned to */
                                        /*      the base version */
    #define VER_NDX_LORESERVE   0xff00  /* beginning of RESERVED entries */
    #define VER_NDX_ELIMINATE   0xff01  /* symbol is to be eliminated */

Before replying, I went into our records to find the
original PSARC case that created Sun's implementation
from 1994. It's evident that the authors expected that
only local symbols would be in version index 0. The
notes from then are nearly 1:1 with what's in the current
Solaris Linker and Libraries manual.

The case we've been discussing, of an undefined global
symbol in a final object, is really unusual, because normally
you'd be unable to link such an object without getting an
unsatisfied symbol error that would fail the link. Remember
that we had to use a weak reference symbol in order to make
it happen. That's very artificial, I think it's safe to
say that it doesn't really happen in useful code, and is
just a weird corner case.

As such, I think it's fair to say that the original authors
weren't thinking about the scenario we've been discussing
at all, hence the naming they used. However, the fact that
index 0 is the only valid index for these weird unsatisfied
globals follows from the way these data structures are defined.
Supporting that is the fact that it matches what the Solaris
link-editor does, and that code is also largely unchanged from
1994. A local symbol will always have a version index of 0,
but a version index of 0 does not always imply a local symbol.
Just 99+% of the time.

Given how obscure this is, I think the name VER_NDX_LOCAL
is OK, if not perfect. Not worth trying to change now, but
something to be aware of.
Comment 21 Ali Bahrami 2025-11-13 01:54:57 UTC
(In reply to Rui Ueyama from comment #19)
> This change completely breaks compatibility with the current and all
> previous versions of the mold linker. Specifically, mold cannot link against
> shared libraries if they are linked with GNU ld after
> f685e3953f9a38a41bbd0a597f9882870cee13d5.
> 
> This appears to be an aesthetic change with no real user benefit, yet it
> already causes a major compatibility issue. It is probably not limited to
> mold, as GNU ld has been emitting the symbol table version the way it
> currently does probably since the very beginning. I don’t think it is worth
> taking the risk. It seems we are breaking user environments with no real
> reward.

That's surprising. It is indeed an obscure thing, and
I'm surprised that any link-editor really cares. Can
you provide details about how it breaks?

I have to assume that this change passed tests before
it was pushed, so I am assuming that the GNU ld itself
is not bothered by the change. Similarly, the Solaris
ld handles either form (we sometimes end up linking
against objects built by GNU ld). It would be helpful
to know why mold is breaking. How does it depend on
the old behavior, and what's throwing it off now?

Correctness is its own reward, and this was really intended
to be a cleanup of a corner case.  I agree that this is not
a hill to die on, but am surprised that it's a hill at all,
so I'd ask that you categorize why it breaks, and then we
can discuss.
Comment 22 H.J. Lu 2025-11-13 01:59:40 UTC
Unversioned symbols apply to both defined and undefined.  The new behavior:

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

extern void show_undef_weak (void) __attribute__((weak));
extern void show_undef (void);

void
foo (void)
{
  show_undef_weak ();
  show_undef ();
  printf ("hello\n");
}
[hjl@gnu-tgl-3 pr33577]$ gcc -fPIC -shared -o libx.so x.c
[hjl@gnu-tgl-3 pr33577]$ objdump -T libx.so

libx.so:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*	0000000000000000              _ITM_deregisterTMCloneTable
0000000000000000      D  *UND*	0000000000000000              show_undef
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) puts
0000000000000000  w   D  *UND*	0000000000000000              __gmon_start__
0000000000000000  w   D  *UND*	0000000000000000              show_undef_weak
0000000000000000  w   D  *UND*	0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*	0000000000000000 (GLIBC_2.2.5) __cxa_finalize
00000000000003b9 g    DF .text	0000000000000020              foo


[hjl@gnu-tgl-3 pr33577]$ 

Both unversioned defined and undefined symbols no longer have the BASE version.
Neither ld nor glibc ld.so use VER_NDX_LOCAL for symbol visibility.
Comment 23 Rui Ueyama 2025-11-13 02:15:30 UTC
We essentially ignore all dynamic symbols with VER_NDX_LOCAL as if they didn't exist in .dynsym at all. I don't exactly remember why we chose to do that, but I believe that was my interpretation of "VER_NDX_LOCAL (0): symbol has local scope".
 Symbols with local scope are generally not visible from other files, so it shouldn't be an unnatural interpretation of the sentence.

We can change mold so that it handles dynamic symbols with VER_NDX_LOCAL in the same manner as GNU ld. That's probably a good idea because mold aims to be as compatible with GNU ld as possible.

However, I'd like you guys to reevaluate the risk of introducing a major compatibility problem with such short notice. It is likely to break many user environments.
Comment 24 Fangrui Song 2025-11-13 06:02:27 UTC
(In reply to Ali Bahrami from comment #17)
> The old behavior was wrong, as was beaten to death in the
> preceding discussion. Hence, not sensible. Undefined
> symbols can't belong to a symbol definition version.
> 
> I do understand that having something that seemed to
> be stable change can be annoying to deal with, but the
> assumption that version 0 was only for definitions isn't
> right. Version 0 is for completely unversioned symbols.



There are follow-up discussions on the patch thread
https://inbox.sourceware.org/binutils/CAMe9rOqZ_+WSJUC_ARjU-o1C5b-CL836kqtS4VpJjXMNjzgJ5w@mail.gmail.com/T/#m08dfe58e142eb12396ce73322bc7cc4bf3584bde
Andreas Schwab and Michael Matz seem to agree with the old behavior (index 1) as well.

While the statement that "non-zero indexes are for defined symbols" is legitimate for Solaris, I maintain my strong arguments that "version 0 (VER_NDX_LOCAL) used to be exclusively for defined symbols" and that "an absent symbol versioning section (like .gnu.version) meant everything was version 1."
I don't think it's worth changing the reasonable GNU ld behavior implemented before 1999.

(
Linker needs to know the versions of undefined symbols in shared object files for two reasons: (see https://reviews.llvm.org/D80059)

- Export a versioned symbol referenced by a shared object, if it is defined in the executable
- Make --no-allow-shlib-undefined work for versioned symbols referenced by a shared object in another object file

I believe in most ELF linkers, zero index for undefined symbols will lead to more code.
)
Comment 25 Ali Bahrami 2025-11-13 15:36:01 UTC
(In reply to Rui Ueyama from comment #23)
> We essentially ignore all dynamic symbols with VER_NDX_LOCAL as if they
> didn't exist in .dynsym at all. I don't exactly remember why we chose to do
> that, but I believe that was my interpretation of "VER_NDX_LOCAL (0): symbol
> has local scope".
>  Symbols with local scope are generally not visible from other files, so it
> shouldn't be an unnatural interpretation of the sentence.

I agree, it seems to follow from the documentation, which
in hindsight seems a bit incomplete or ambiguous.

At best, VER_NDX_LOCAL is derived information, based on the
actual binding, which is in the symbol's st_info field. I think
it would be better to key off that. You probably do, for objects
that don't have a versym section (aren't versioned). Or do you
not encounter non-versioned objects?

 
> We can change mold so that it handles dynamic symbols with VER_NDX_LOCAL in
> the same manner as GNU ld. That's probably a good idea because mold aims to
> be as compatible with GNU ld as possible.

That's probably a good idea for general correctness as well.
Versioning augments the basic symbol, but isn't intended to
contradict it.

> 
> However, I'd like you guys to reevaluate the risk of introducing a major
> compatibility problem with such short notice. It is likely to break many
> user environments.

When different implementations of a written spec make different
interpretations like this, it can be hard to spot, and hard to
fix, particularly when a lot of time passes. It seems that we've
found another one of those. Had this been understood as a compatibility
concern, we wouldn't have gone there, and now that we understand
that it is, and how, I won't stand in the way of rolling it back.

Perhaps it could be wrapped in an "#ifdef __sun" for now?
The correctness of the GNU choice (assigning an undefined
reference to a VERDEF index) is questionable, so this is
entirely about not breaking existing tools. It would be
nice and tidy if objects made for Solaris could follow our
implementation. On the other hand, we seem to handle it either
way, so I don't insist.
Comment 26 H.J. Lu 2025-11-13 22:13:04 UTC
Created attachment 16461 [details]
A testcase for VER_NDX_GLOBAL

This is a testcase for VER_NDX_GLOBAL with

asm (".symver foo_base, foo@");

VER_NDX_GLOBAL is used to create the base version to provide an unversioned
compatible symbol.
Comment 27 H.J. Lu 2025-11-13 22:21:10 UTC
As the first step, we should clarify exactly what version index 0, VER_NDX_LOCAL,
means.  Currently both ld and ld.so in glibc don't treat version index 0 as hidden
visibility.  In ld, only ia64 has local dynamic symbols:

[hjl@gnu-tgl-3 ia64]$ readelf -VDsW local1.so 

Symbol table for image contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000000001f0    16 FUNC    LOCAL  DEFAULT    7 hidden
     2: 0000000000010348     8 OBJECT  GLOBAL DEFAULT   10 foo@@VERS_1
     3: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  ABS VERS_1

Version symbols section '.gnu.version' contains 4 entries:
 Addr: 0x000000000000018e  Offset: 0x0000018e  Link: 2 (.dynsym)
  000:   0 (*local*)       0 (*local*)       2 (VERS_1)        2 (VERS_1)     

Version definition section '.gnu.version_d' contains 2 entries:
 Addr: 0x0000000000000198  Offset: 0x00000198  Link: 3 (.dynstr)
  000000: Rev: 1  Flags: BASE  Index: 1  Cnt: 1  Name: local1.so
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: VERS_1
[hjl@gnu-tgl-3 ia64]$ 

hidden is a local dynamic symbol with VER_NDX_LOCAL.

I think the spec should say something like that a global/weak defined/undefined,
with version index 0 is a symbol without version.
Comment 28 Sam James 2025-11-13 23:47:46 UTC
Reopening while we discuss.
Comment 29 Ali Bahrami 2025-11-14 00:28:42 UTC
(In reply to H.J. Lu from comment #27)
> As the first step, we should clarify exactly what version index 0,
> VER_NDX_LOCAL,
> means.  Currently both ld and ld.so in glibc don't treat version index 0 as
> hidden
> visibility.
...
> hidden is a local dynamic symbol with VER_NDX_LOCAL.
> 
> I think the spec should say something like that a global/weak
> defined/undefined,
> with version index 0 is a symbol without version.


In my opinion, the only basis for treating a symbol
as hidden should be that it specifies hidden visibility
it it's st_other field, as extracted by ELF[32|64]_ST_VISIBILITY.
Similarly, the only basis for treating a symbol as local
is that it has STB_LOCAL visibility. The version may reflect
those things, but it doesn't determine them.

While symbols with VER_NDX_LOCAL are almost always local
(as opposed to globals that actually have hidden visibility),
we know there are other cases where it applies, as we've been
discussing. So I would say that index 0 is the index to be used
when no other version index applies. If I could go back to 1994
with this discussion fresh in my mind, I might suggest that
it be named VER_NDX_NONE.

I don't think we should rename it now of course, that
ship has sailed.
Comment 30 Ali Bahrami 2025-11-14 00:33:47 UTC
(In reply to Ali Bahrami from comment #29)
> Similarly, the only basis for treating a symbol as local
> is that it has STB_LOCAL visibility.

s/visiblity/binding

And of course a global with hidden visibility behaves
a lot like an actual local binding in many ways. I really
was just trying to say that the symbol itself is the place
to look for binding and visibility, not the version.
Comment 31 H.J. Lu 2025-11-14 00:42:15 UTC
(In reply to Ali Bahrami from comment #29)
> In my opinion, the only basis for treating a symbol
> as hidden should be that it specifies hidden visibility
> it it's st_other field, as extracted by ELF[32|64]_ST_VISIBILITY.
> Similarly, the only basis for treating a symbol as local
> is that it has STB_LOCAL visibility. The version may reflect
> those things, but it doesn't determine them.
> 
> While symbols with VER_NDX_LOCAL are almost always local
> (as opposed to globals that actually have hidden visibility),
> we know there are other cases where it applies, as we've been
> discussing. So I would say that index 0 is the index to be used
> when no other version index applies. If I could go back to 1994
> with this discussion fresh in my mind, I might suggest that
> it be named VER_NDX_NONE.
> 
> I don't think we should rename it now of course, that
> ship has sailed.

Rename to VER_NDX_NONE may be too late.  But

https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/linkers-libraries/version-symbol-section.html

can be updated to clarify what version index 0 really means for unversioned
global/weak defined/undefined symbols.
Comment 32 Rui Ueyama 2025-11-14 02:27:59 UTC
In the meantime, can we roll back f685e3953f9a38a41bbd0a597f9882870cee13d5? If we don’t, significant breakage across many environments is almost certainly guaranteed when GNU binutils 2.46 is released for those who upgrade binutils and mold individually.

I will make a change to mold so that it can consume shared libraries linked with that particular change, but we cannot expect users to upgrade all of their tools at the same time.
Comment 33 Ali Bahrami 2025-11-14 04:46:01 UTC
(In reply to H.J. Lu from comment #31)
> Rename to VER_NDX_NONE may be too late.  But
> 
> https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/
> linkers-libraries/version-symbol-section.html
> 
> can be updated to clarify what version index 0 really means for unversioned
> global/weak defined/undefined symbols.

I can do that. It may take awhile for the change to appear
online, but will happen in due course.
Comment 34 Fangrui Song 2025-11-15 18:52:46 UTC
Michael on the binutils mailing list pointed out that an old document from Solaris https://shrubbery.net/solaris9ab/SUNWdev/LLM/p45.html doesn't mention version index (0) for unversioned undefined symbols.

Citing a Solaris specification is not convincing, given that the GNU ld versioning scheme was implemented in the 1990s and its subsequent evolution and divergence from the original design are unclear.

However, in practical testing using a 2019 GNU ld version (before sourceware.org/PR26002 , I forgot that I had reported this issue), I observed that it produced index 0 for unversioned undefined symbols in several manually crafted cases.

---

The GNU and Linus Standard Base documentation is ambiguous about the version index for unversioned undefined symbols.
The current specification at sourceware.org/gnu-gabi/program-loading-and-dynamic-linking.txt defines VER_NDX_LOCAL (0) as "The symbol is private, and is not available outside this object."

However, this naming is misleading for undefined symbols. As suggested in discussions, VER_NDX_LOCAL should conceptually be VER_NDX_NONE and apply to unversioned undefined symbols as well.

However, this alone does not justify the GNU ld producer change.

GNU ld has used index 0 for unversioned undefined symbols both before version 2.35 (see sourceware.org/PR26002 in 2020) and in the upcoming 2.46 release (see sourceware.org/PR33577).

Therefore, I think it makes sense for other ELF linkers to adopt index 0 for unversioned undefined symbols.
Comment 35 Michael Matz 2025-11-17 14:37:18 UTC
Well, citing a Sun specification from the 90's is relevant, because Ali used the
ARCs to point at the original intent of NDX_LOCAL (and it being more
along the line of "unknown version"), and the docu shows that that wasn't
explicitely put down externally back then (i.e. added only later there).

Now, whatever the original intent may have been (and I of course agree, it
absolutely makes sense to use '0' as the nothing-known index for mere
references), it wasn't what was written in most places, which only was

  NDX_LOCAL - local scope [full stop]

.  There really isn't much wiggle room in interpreting that single line (and to remind, the GNU version stuff was designed in large part by getting inspired by relevant and available Sun docs).  Either way, that is what several binary tools
implemented over a long time, and we can't now willy-nilly change that without
some allowance period for that change.  I will note that the original report
from Rainer here was about a mere perceived inconsistency in objdump output,
not any specific breakage.  It seems a disservice to fix that confusion by
introducing real breakage with other linkers.

So, IMHO: we (GNU ld) should continue to emit NDX_GLOBAL for the undefined
symbols, as we had.  Other linkers should be changed to not particularly care
for either NDX_LOCAL or NDX_GLOBAL for undefined symbols (GNU ld already is fine
with NDX_LOCAL on such syms).  Then, wait a release, then change GNU ld to emit
NDX_LOCAL.
Comment 36 Ali Bahrami 2025-11-17 18:59:16 UTC
(In reply to H.J. Lu from comment #31)
> Rename to VER_NDX_NONE may be too late.  But
> 
> https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/
> linkers-libraries/version-symbol-section.html
> 
> can be updated to clarify what version index 0 really means for unversioned
> global/weak defined/undefined symbols.

I apologize for not spotting this last week when we
first discussed this, but I think the Solaris docs
do spell out the details for index 0 pretty thoroughly.
It's true that the table says simply "Symbol has local scope",
and that text goes back to the original implementation, but
that table is followed by a bulleted list that gets into
the details:

    > A symbol may be assigned the special reserved
    > index 0. This index can be assigned for any of
    > the following reasons.
    >
    > - A non-global symbol is always assigned VER_NDX_LOCAL.
    >   However, this is rare in practice. Versioning sections
    >   are usually created only in conjunction with the dynamic
    >   symbol table, .dynsym, which only contains global symbols.
    >
    > - A global symbol defined within an object that does not
    >   have a SHT_SUNW_verdef version definition section.
    >
    > - An undefined global symbol defined within an object that
    >   does not have a SHT_SUNW_verneed version dependency section.
    >   Or, an undefined global symbol defined within an object in
    >   which the version dependency section does not assign version
    >   indexes.
    >
    > - The first entry of a symbol table is always NULL. This
    >   entry always receives VER_NDX_LOCAL, however the value
    >   has no particular meaning.

The third bullet ("An undefined global symbol") covers the
situation we've been discussing, and it seems this discussion
has been retracing those steps.

This text is not found in the S9 version of the Solaris LLM
references earlier, nor in the Solaris 10 FCS version from 2005,
but is found in the Solaris 11 FCS version from 2011. That's why
I didn't spot it earlier --- my response was based on reading
the cited link, and clearly I should have looked at the latest
docs as well. However, there's a decent chance that I wrote this
addition (between 2005 and 2011, when S11 shipped, so 16-20 years
ago), so it's probably useful that we've independently verified
what it's saying.
Comment 37 Ali Bahrami 2025-11-17 19:11:35 UTC
I should have said above that the appearance of that
added explanatory text came after the GNU implementation
had already been written, so this isn't a case
where something obvious was ignored. Rather, it's
that documentation was later bolstered to more accurately
capture what was intended, but the GNU implementation
was already out, and it took a very long time for anyone
to notice the mismatch.
Comment 38 H.J. Lu 2025-11-17 22:07:56 UTC
(In reply to Michael Matz from comment #35)
> Well, citing a Sun specification from the 90's is relevant, because Ali used
> the
> ARCs to point at the original intent of NDX_LOCAL (and it being more
> along the line of "unknown version"), and the docu shows that that wasn't
> explicitely put down externally back then (i.e. added only later there).
> 
> Now, whatever the original intent may have been (and I of course agree, it
> absolutely makes sense to use '0' as the nothing-known index for mere
> references), it wasn't what was written in most places, which only was
> 
>   NDX_LOCAL - local scope [full stop]
> 
> .  There really isn't much wiggle room in interpreting that single line (and
> to remind, the GNU version stuff was designed in large part by getting
> inspired by relevant and available Sun docs).  Either way, that is what
> several binary tools
> implemented over a long time, and we can't now willy-nilly change that
> without

ld and ld.so in glibc never use VER_NDX_LOCAL to change symbol binding.
Comment 39 Sourceware Commits 2025-11-17 22:17:15 UTC
The master branch has been updated by H.J. Lu <hjl@sourceware.org>:

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=e891646a32ac9adcff944d05b14054523f752728

commit e891646a32ac9adcff944d05b14054523f752728
Author: H.J. Lu <hjl.tools@gmail.com>
Date:   Sat Nov 15 06:59:56 2025 +0800

    gold: Treat symbols with version index 0 as unversioned
    
    Oracle Solaris 11.4 Linker and Libraries Guide:
    
    https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/linkers-libraries/version-symbol-section.html
    
    defines VER_NDX_LOCAL to 0 with a comment, "Symbol has local scope".  This
    leads to different interpretations by different linker implementations.
    However Solaris as well as ld and ld.so in glibc always treat symbols
    with version index 0 as unversioned symbols with global binding.  As
    discussed in
    
    https://sourceware.org/bugzilla/show_bug.cgi?id=33577
    
    in hindsight, VER_NDX_NONE might be a better name.  Ali from Oracle is
    working on clarifying what version index 0 really means for unversioned
    symbols with global binding.  In the meantime, update gold to treat
    symbols with version index 0 as unversioned with global binding.
    
    elfcpp/
    
            PR gold/33577
            * elfcpp.h (VER_NDX_LOCAL): Update comments.
            (VER_NDX_GLOBAL): Likewise.
    
    gold/
    
            PR gold/33577
            * dynobj.cc (Versions::symbol_section_contents): Set unversioned
            symbol version index to VER_NDX_LOCAL.
            * symtab.cc (Symbol_table::add_from_dynobj): Don't check
            VER_NDX_LOCAL.
            * testsuite/Makefile.am (check_SCRIPTS): Add ver_test_pr33577.sh.
            (check_DATA): Add ver_test_pr33577a.syms and
            ver_test_pr33577b.syms.
            (ver_test_pr33577a.syms): New rule.
            (ver_test_pr33577.so): Likewise.
            (ver_test_pr33577b.syms): Likewise.
            (ver_test_pr33577): Likewise.
            * testsuite/Makefile.in: Regenerated.
            * testsuite/ver_matching_test.sh: Updated to checking missing
            Base version.
            * testsuite/ver_test_14.sh (check_missing): New.
            Updated to check missing Base version.
            * testsuite/ver_test_pr33577.sh: New fille.
            * testsuite/ver_test_pr33577a.c: Likewise.
            * testsuite/ver_test_pr33577b.c: Likewise.
    
    Signed-off-by: H.J. Lu <hjl.tools@gmail.com>
Comment 40 Michael Matz 2025-11-18 13:20:24 UTC
(In reply to H.J. Lu from comment #38)
> (In reply to Michael Matz from comment #35)
> > inspired by relevant and available Sun docs).  Either way, that is what
> > several binary tools
> > implemented over a long time, and we can't now willy-nilly change that
> > without
> 
> ld and ld.so in glibc never use VER_NDX_LOCAL to change symbol binding.

And your point is that these are the only binary tools that exist?  Or why do
you mention that?
Comment 41 Michael Matz 2025-11-18 13:26:31 UTC
People in the wild already noticed the problem with ld.bfd now producing
incorrect output (according to https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-PDA/LSB-DA.junk/symversion.html , not some Sun document that, strictly speaking,
isn't the gosple for the GNU variant of symbol versioning): https://bugzilla.redhat.com/show_bug.cgi?id=2415065 .

I find the current way of dealing with this fairly reckless :-/
Comment 42 Michael Matz 2025-11-18 13:44:11 UTC
(In reply to Ali Bahrami from comment #36)

> I apologize for not spotting this last week when we
> first discussed this, but I think the Solaris docs
> do spell out the details for index 0 pretty thoroughly.

We noted that here:
  https://sourceware.org/pipermail/binutils/2025-November/145608.html
with reply here:
  https://sourceware.org/pipermail/binutils/2025-November/145643.html .

> The third bullet ("An undefined global symbol") covers the
> situation we've been discussing, and it seems this discussion
> has been retracing those steps.
> 
> This text is not found in the S9 version of the Solaris LLM
> references earlier, nor in the Solaris 10 FCS version from 2005,
> but is found in the Solaris 11 FCS version from 2011. That's why
> I didn't spot it earlier --- my response was based on reading
> the cited link, and clearly I should have looked at the latest
> docs as well. However, there's a decent chance that I wrote this
> addition (between 2005 and 2011, when S11 shipped, so 16-20 years
> ago), so it's probably useful that we've independently verified
> what it's saying.

I don't think any independendness comes into play here.  The ones arguing
for changing the GNU versioning implementation ultimately always come from
reading the current Solaris docu, i.e. with the clarifications of NDX_LOCAL.

Btw, people also try to read into it that it's fine to use NDX_LOCAL also
for _defined_ symbols.  AFAICS even the clarification in the Solaris>=11
docu only accepts that when there's no verdef section in the defining file.

What's your take on that?  Is a NDX_LOCAL, defined STB_GLOBAL symbol in
a file that has the verdef section (GNU or Sun variant) available to resolve
against, or not?  Should it be?
Comment 43 Ali Bahrami 2025-11-18 17:58:12 UTC
(In reply to Michael Matz from comment #42)
> We noted that here:
>   https://sourceware.org/pipermail/binutils/2025-November/145608.html
> with reply here:
>   https://sourceware.org/pipermail/binutils/2025-November/145643.html .

I see, and I agree with the comment in the second one that those
words were a relatively later addition, though they do reflect how
the Solaris ld has always done it. My fuzzy memory of this
is that I added them while working on

    PSARC 2008/603 ELF objects to adopt GNU-style Versym indexes

The Sun style originally did not put version indexes for externally
defined symbols into the versym section, setting those values all
to 0, so while one could deduce that an object had a version dependency
on another object, we could not discern specifically which symbols
were responsible. We decided to adopt that part of the GNU extensions,
and in doing that work, it dawned on me that index 0 has implications
beyond local symbols, leading to questions, and a doc update.


> I don't think any independendness comes into play here.  The ones arguing
> for changing the GNU versioning implementation ultimately always come from
> reading the current Solaris docu, i.e. with the clarifications of NDX_LOCAL.

Great to know. I continue to be grateful for how closely GNU has
stuck to ELF, as well as to these Sun additions, but I try not to
take it for granted.

> Btw, people also try to read into it that it's fine to use NDX_LOCAL also
> for _defined_ symbols.  AFAICS even the clarification in the Solaris>=11
> docu only accepts that when there's no verdef section in the defining file.

The use of NDX_LOCAL for globals is very limited.
One might might interpret this as a license:

    - A global symbol defined within an object that does
      not have a SHT_SUNW_verdef version definition section.

However, I think that rule is intended to describe a dependency
that doesn't have a verdef. Note that the text below this
list goes on to say:

    Versions defined by an object are assigned version indexes
    starting at 1 and incremented by 1 for each version. Index 1
    is reserved for the first global version. If the object does
    not have a SHT_SUNW_verdef version definition section, then
    all the global symbols defined by the object receive index 1.
    If the object does have a version definition section, then
    VER_NDX_GLOBAL simply refers to the first such version

Forgive the verbosity, but a concrete example might be useful.
This hello world program is not itself versioned, but it links
to libc, which is, and so, all of its own definitions end up
in version 1 even though it, itself, does not have a verdef,

    % cc hello.c
    % elfdump -v a.out

    Version Needed Section:  .SUNW_version
      index  file       version    
        [2]  libc.so.1  SYSVABI_1.3

    % elfdump -sN.dynsym a.out

    Symbol Table Section:  .dynsym
      index      value size  type bind oth ver shndx    name
        [0]          0    0  NOTY LOCL  D    0 UNDEF    
        [1]  0x8060d7c  0x4  OBJT GLOB  X    1 .data    __xargc
        [2]  0x8050968    0  FUNC GLOB  D    2 UNDEF    printf
        [3]  0x8050918    0  OBJT GLOB  D    1 .plt         _PROCEDURE_LINKAGE_TABLE_
        [4]  0x8060d5c  0x4  OBJT WEAK  X    1 .data    environ
        [5]  0x8060c04    0  OBJT GLOB  D    1 .dynamic _DYNAMIC
        [6]  0x8060db0    0  OBJT GLOB  D    1 .data    _edata
        [7]  0x8060d78  0x4  OBJT GLOB  X    1 .data    ___Argv
        [8]  0x8050b90 0x1b  FUNC GLOB  D    1 .init    _init
        [9]  0x8050bc7    0  OBJT GLOB  D    1 .fini    _etext
       [10]          0    0  NOTY GLOB  D    1 ABS      __fsr_init_value
       [11]  0x8050b60 0x2f  FUNC GLOB  D    1 .text    main
       [12]  0x8060d5c  0x4  OBJT GLOB  X    1 .data    _environ
       [13]  0x8060bc8    0  OBJT GLOB  P    1 .got     _GLOBAL_OFFSET_TABLE_
       [14]  0x8060d80  0x4  OBJT GLOB  X    1 .data    __xargv
       [15]  0x8060da8  0x4  OBJT GLOB  D    1 .data    __xpg4
       [16]  0x80508fc  0x4  OBJT GLOB  X    1 .rodata  _lib_version
       [17]  0x8050958    0  FUNC GLOB  D    2 UNDEF    _exit
       [18]  0x8050948    0  FUNC GLOB  D    2 UNDEF    atexit
       [19]  0x8050928    0  FUNC GLOB  D    2 UNDEF    __fpstart
       [20]  0x8060dac  0x4  OBJT GLOB  D    1 .data    __xpg6
       [21]  0x8050938    0  FUNC GLOB  D    2 UNDEF    exit
       [22]  0x8060db0    0  OBJT GLOB  D    1 .data    _end
       [23]  0x8050980 0x86  FUNC GLOB  X    1 .text    _start
       [24]  0x8050bac 0x1b  FUNC GLOB  D    1 .fini    _fini
       [25]  0x8060d60 0x18  OBJT GLOB  X    1 .data    __environ_lock
       [26]  0x8060d84  0x4  OBJT GLOB  X    1 .data    __longdouble_used

As long as versioning is in play, global definitions that
are not otherwise directed to a different version via a
mapfile/linker-script, end up in version 1. And since nearly
everything has a dependency on libc, which is versioned, it's
hard to make an object on Solaris that doesn't assign these
cases to index 1

Note that Rainer manages some modern Solaris systems in the
gcc test farm, and it should be possible to get shell access
in order to poke at these details if that would be useful.
    

> What's your take on that?  Is a NDX_LOCAL, defined STB_GLOBAL symbol in
> a file that has the verdef section (GNU or Sun variant) available to resolve
> against, or not?  Should it be?

I'm not sure those symbols are useful. Consider the case that
Rainer presented at the start of this, a weak reference, for which
no definition was found during the link-edit. If symbol resolution
is done for it, nothing will be found. However, it seems that we
do support it.

    % cc -m64 -Kpic -G -o vers33.so vers33.c -Mvers33.map
    % elfdump -L vers33.so

    Procedure Linkage Table Section:  .plt
      index   addr  GOT-index  GOT-addr
        [1]  0x960        [4]  0x100a08  show_undef_weak

    % dis -F foo vers33.so 
    disassembly for vers33.so

    foo()
        foo:                    55                 pushq  %rbp
        foo+0x1:                48 8b ec           movq   %rsp,%rbp
        foo+0x4:                48 8b 05 75 00 10  movq   +0x100075(%rip),%rax      <0x100a00>
                                00 
        foo+0xb:                48 85 c0           testq  %rax,%rax
        foo+0xe:                74 05              je     +0x5      <foo+0x15>
        foo+0x10:               e8 cb ff ff ff     call   -0x35     <0x960>
        foo+0x15:               c9                 leave  

I created a hello world program to call foo(), and
debugging diagnostics show that we chase it and
fail to find anything:

    % LD_DEBUG=symbols ./a.out |& grep show_undef
    09599: symbol=show_undef_weak;  lookup in file=a.out  [ ELF ]
    09599: symbol=show_undef_weak;  lookup in file=./vers33.so  [ ELF ]
    09599: symbol=show_undef_weak;  lookup in file=/lib/64/libc.so.1  [ ELF ]

I think this says that such symbols are resolved, but
there's likely no resulting benefit.

-----

I'd also like to say that I fully agree that these changes
to the GNU toolchain do need to be phased in over time, seeding
the capability and then waiting for awhile before switching over
to using it. The chaos of an abrupt switch for such a small thing
just isn't worth the cost, but it would be nice to get there gradually.
Our original ask, that is be changed immediately, was due to thinking
it was just a minor bookkeeping matter. We've since been educated to
its much larger impact.
Comment 44 H.J. Lu 2025-11-18 22:16:08 UTC
ld.so in glibc always determines bindings of VER_NDX_LOCAL symbols by st_other.
Linux linkers should be the same.
Comment 45 H.J. Lu 2025-11-18 23:53:51 UTC
The old spec specifies symbols with version index 0 to local binding.  However, it
is never implemented on Solaris, ld nor ld.so in Linux.  How about adding a linker
option to use version index 1 for unversioned defined symbol?  In the meantime,
all Linux linkers should be changed to follow ld and ld.so.
Comment 46 Fangrui Song 2025-11-19 04:45:38 UTC
(In reply to Michael Matz from comment #41)
> People in the wild already noticed the problem with ld.bfd now producing
> incorrect output (according to
> https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-PDA/LSB-DA.junk/
> symversion.html , not some Sun document that, strictly speaking,
> isn't the gosple for the GNU variant of symbol versioning):
> https://bugzilla.redhat.com/show_bug.cgi?id=2415065 .
> 
> I find the current way of dealing with this fairly reckless :-/

https://bugzilla.redhat.com/show_bug.cgi?id=2415065 is an instance that GNU ld starts to generates unversioned defined symbols of index 0 in the dynamic symbol table, which gets rejected by LLD (since at least 2018). LLD probably should emit a warning in such a case.


This GNU ld behavior change for defined symbols is unexpected (should not emit localized symbols to .dynsym - at least in the general case, is there an exception for ia64 (unsupported by llvm)?) and we should request a revert.
Comment 47 Sam James 2025-11-19 11:52:27 UTC
It's especially a problem as new Rust defaults to lld use. Can we have a revert please until we have things figured out?
Comment 48 Rui Ueyama 2025-11-19 12:18:33 UTC
On behalf of mold users, I’d like to second Sam’s revert request. This is an abrupt change that would break many users’ development environments. I hope we can arrive at a practical decision.
Comment 49 Michael Matz 2025-11-19 14:14:29 UTC
(In reply to Ali Bahrami from comment #43)

>     % cc hello.c
>     % elfdump -v a.out
> 
>     Symbol Table Section:  .dynsym
>       index      value size  type bind oth ver shndx    name
>        [11]  0x8050b60 0x2f  FUNC GLOB  D    1 .text    main

In the current git of GNU ld, all the above exported symbols (we have to use
-E here to export globals from executables) are now VER_NDX_LOCAL:

Symbol table '.dynsym' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
...
    11: 0000000000401136    50 FUNC    GLOBAL DEFAULT   13 main
...
Version symbols section '.gnu.version' contains 12 entries:
 Addr: 0x00000000004005be  Offset: 0x000005be  Link: 5 (.dynsym)
  000:   0 (*local*)       2 (GLIBC_2.34)    3 (GLIBC_2.2.5)   0 (*local*)    
  004:   0 (*local*)       0 (*local*)       0 (*local*)       0 (*local*)    
  008:   0 (*local*)       0 (*local*)       0 (*local*)       0 (*local*)    

> As long as versioning is in play, global definitions that
> are not otherwise directed to a different version via a
> mapfile/linker-script, end up in version 1.

Agreed.  However that's not what the current git implementation of the fix
for the initially reported problem here does.  It _always_ uses NDX_LOCAL
for everything not versioned, undefined or defined, independend of symbols
binding.

I think there should have been no change whatsoever for defined symbols.
Comment 50 Michael Matz 2025-11-19 14:20:05 UTC
(In reply to H.J. Lu from comment #44)
> ld.so in glibc always determines bindings of VER_NDX_LOCAL symbols by
> st_other.
> Linux linkers should be the same.

Or you could just admit that ld.so was incompletely implementing the spec
by not looking at the version index when its there.

(In reply to H.J. Lu from comment #45)
> The old spec specifies symbols with version index 0 to local binding. 
> However, it is never implemented on Solaris, ld nor ld.so in Linux.

But by _everything_ except ld.bfd and glibc ld.so.  And 

> How about adding a linker option to use version index 1 for unversioned
> defined symbol?

I don't see how a linker option to not produce buggy output is useful.  But perhaps one to do produce buggy output.  I.e. sure, but default to use NDX_GLOBAL
for defined STB_GLOBAL symbols.

> In the meantime, all Linux linkers should be changed to follow ld and ld.so.

Why?  The others follow the spec.
Comment 51 Michael Matz 2025-11-19 16:50:21 UTC
So the below would revert the questionable behaviour, i.e. return back to
generating version index VER_NDX_GLOBAL for defined global symbols (using index
0 for undefined symbols, the point of this bug report, remains unchanged).

To not regress the testsuite as is, it would also need reversion of
2be0f2da ("readelf: Display the base symbol version as empty string")
that always prints "foo@" for VER_NDX_GLOBAL symbols.  That could also be done
by adjusting some of the testcases to accept that syntax.

---
    Use version index 1 for defined symbols
    
    It's reasonable to use version index 0 for undefined
    symbols, so let's continue doing that.  For defined (global)
    symbols that aren't otherwise versioned continue using
    VER_NDX_GLOBAL (partly reverting behaviour introduced in
    commit f685e395).
    
    bfd/
            PR ld/33577
            * elflink.c (elf_link_output_extsym): Don't set noversion
            for defined syms.
    
    ld/
            PR ld/33577
            * ld-elfvers/vers16.dsym: Add back the "Base" version for
            defined syms.

diff --git a/bfd/elflink.c b/bfd/elflink.c
index ec3ad9735fe..7d284c08c00 100644
--- a/bfd/elflink.c
+++ b/bfd/elflink.c
@@ -11158,13 +11158,7 @@ elf_link_output_extsym (struct bfd_hash_entry *bh, void *data)
          else
            {
              if (h->verinfo.vertree == NULL)
-               {
-                 iversym.vs_vers = 1;
-                 if (elf_tdata (flinfo->output_bfd)->cverdefs == 0)
-                   /* Defined symbol has no version if there is no
-                      linker version script.  */
-                   noversion = true;
-               }
+               iversym.vs_vers = 1;
              else
                iversym.vs_vers = h->verinfo.vertree->vernum + 1;
              if (flinfo->info->create_default_symver)
diff --git a/ld/testsuite/ld-elfvers/vers16.dsym b/ld/testsuite/ld-elfvers/vers16.dsym
index 076d0eb07ed..a32abd7aad9 100644
--- a/ld/testsuite/ld-elfvers/vers16.dsym
+++ b/ld/testsuite/ld-elfvers/vers16.dsym
@@ -1,2 +1,2 @@
-[0-9a-f]+ g +DF (\.text|\.opd|\*ABS\*) [0-9a-f]+ +(0x[0-9a-f]+ )?_?show_bar
+[0-9a-f]+ g +DF (\.text|\.opd|\*ABS\*) [0-9a-f]+( +Base +)? +(0x[0-9a-f]+ )?_?show_bar
 [0-9a-f]+ +DF \*UND\*  [0-9a-f]+ +(0x[0-9a-f]+ )?_?show_foo
Comment 52 Jan Beulich 2025-11-20 08:00:15 UTC
(In reply to Michael Matz from comment #51)
> So the below would revert the questionable behaviour, i.e. return back to
> generating version index VER_NDX_GLOBAL for defined global symbols (using
> index
> 0 for undefined symbols, the point of this bug report, remains unchanged).
> 
> To not regress the testsuite as is, it would also need reversion of
> 2be0f2da ("readelf: Display the base symbol version as empty string")
> that always prints "foo@" for VER_NDX_GLOBAL symbols.  That could also be
> done
> by adjusting some of the testcases to accept that syntax.

That would be best, imo. Okay in that shape.
Comment 53 Nick Clifton 2025-11-20 08:47:05 UTC
I also think that we should revert back to the original behaviour.  Or ... provide a new linker option that enables the new behaviour, but which defaults to the old behaviour.  

This default could be changed in the future once agreement is reached and all the linkers are able to handle the new behaviour.

H.J. / Michael - please could one of you make this happen ?
Comment 54 Michael Matz 2025-11-20 15:36:06 UTC
https://sourceware.org/pipermail/binutils/2025-November/145783.html

for the series (my mailer unfortunately overwrote the message-id and references header, so the four messages don't thread).
Comment 55 Fangrui Song 2025-11-22 20:13:46 UTC
The regression: When a shared object uses versioned symbols from its dependencies but omits a version script, its exported defined symbols are incorrectly indexed as 0 (should be index 1).

The solution: Michael's patch should resolve this. While reverting the preceding change might be simpler, Michael has done the necessary work to implement the proper code fix.

---

I don't think a new option is justified to select index 0 / 1 for unversioned undefined symbols.
Comment 56 Sourceware Commits 2025-11-24 00:15:21 UTC
The master branch has been updated by H.J. Lu <hjl@sourceware.org>:

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=beab972c07d53ba1b0cafc2d16420c40a799bbfd

commit beab972c07d53ba1b0cafc2d16420c40a799bbfd
Author: H.J. Lu <hjl.tools@gmail.com>
Date:   Sat Nov 15 11:43:45 2025 +0800

    elf: Verify base symbol version works properly
    
    Add a testcase to verify that VER_NDX_GLOBAL can be used with
    
    asm (".symver foo_base, foo@");
    
    to create the base version to provide an unversioned compatible symbol.
    
            PR ld/33577
            * testsuite/ld-elfvers/pass.out: New file.
            * testsuite/ld-elfvers/pr33577-unversioned.c: Likewise.
            * testsuite/ld-elfvers/pr33577-unversioned.rd: Likewise.
            * testsuite/ld-elfvers/pr33577-versioned.c: Likewise.
            * testsuite/ld-elfvers/pr33577-versioned.rd: Likewise.
            * testsuite/ld-elfvers/pr33577.map: Likewise.
            * testsuite/ld-elfvers/pr33577a.c: Likewise.
            * testsuite/ld-elfvers/pr33577b.c: Likewise.
            * testsuite/ld-elfvers/vers.exp: Run ld/33577 tests.
    
    Signed-off-by: H.J. Lu <hjl.tools@gmail.com>
Comment 57 Sourceware Commits 2025-11-24 14:33:55 UTC
The master branch has been updated by Michael Matz <matz@sourceware.org>:

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=78894bcb483d45c4d7be5ef9499ec017a9727079

commit 78894bcb483d45c4d7be5ef9499ec017a9727079
Author: Michael Matz <matz@suse.de>
Date:   Wed Nov 19 16:32:51 2025 +0100

    Use version index 1 for defined symbols
    
    It's reasonable to use version index 0 for undefined
    symbols, so let's continue doing that.  For defined (global)
    symbols that aren't otherwise versioned continue using
    VER_NDX_GLOBAL (partly reverting behaviour introduced in
    commit f685e395).
    
    bfd/
            PR ld/33577
            * elflink.c (elf_link_output_extsym): Don't set noversion
            for defined syms.
    
    ld/
            PR ld/33577
            * ld-elfvers/vers16.dsym: Add back the "Base" version for
            defined syms.
Comment 58 Sourceware Commits 2025-11-30 07:34:16 UTC
The master branch has been updated by H.J. Lu <hjl@sourceware.org>:

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=94ebb7810ebd966aa857aa55c5bdc34382f4572b

commit 94ebb7810ebd966aa857aa55c5bdc34382f4572b
Author: H.J. Lu <hjl.tools@gmail.com>
Date:   Fri Nov 28 08:05:18 2025 +0800

    elf: Properly place base symbols in DT_GNU_HASH and DT_HASH
    
    The base symbol, a symbol with the empty version string, created by
    
    asm (".symver foo_base, foo@");
    
    is used to provide a compatibility symbol in a versioned shared library
    for binaries linked against the previous unversioned shared library.
    The dynamic linker will pick the first match to resolve the unversioned
    symbol reference.  If the newest version, VERS_1,
    
    asm (".symver foo_v1, foo@@VERS_1");
    
    is picked before the base symbol in DT_GNU_HASH and DT_HASH, foo@@VERS_1,
    instead of foo@, will be used to resolve the unversioned reference.
    Properly place base symbols in DT_GNU_HASH and DT_HASH so that they will
    be picked first.
    
    Also check defined function symbol, foo, and undefined function symbol,
    bar, separately to support different dynamic symbol orders.
    
    bfd/
    
            PR ld/33577
            PR ld/33673
            * elf-bfd.h (elf_link_hash_entry): Add base_symbol.
            (elf_link_hash_table): Add has_base_symbols.
            * elflink.c (_bfd_elf_merge_symbol): Set base_symbol and
            has_base_symbols if the version string is empty.
            (collect_gnu_hash_codes): Add base_symbol.
            (elf_gnu_hash_process_symidx): Skip if base symbol doesn't match.
            (bfd_elf_size_dynsym_hash_dynstr): If there are base symbols,
            output base symbols first in DT_GNU_HASH.
            (elf_outext_info): Add base_symbol.
            (elf_link_output_extsym): Skip if base symbol doesn't match.
            (_bfd_elf_final_link): If there are base symbols, output base
            symbols last in DT_HASH.
    
    ld/
    
            PR ld/33577
            PR ld/33673
            * testsuite/ld-elfvers/pr33577-unversioned.rd: Removed.
            * testsuite/ld-elfvers/pr33577-versioned.rd: Likewise.
            * testsuite/ld-elfvers/pr33577-unversioned-a.rd: New file.
            * testsuite/ld-elfvers/pr33577-unversioned-b.rd: Likewise.
            * testsuite/ld-elfvers/pr33577-versioned-a.rd: Likewise.
            * testsuite/ld-elfvers/pr33577-versioned-b.rd: Likewise.
            * ld-elfvers/vers.exp (base_symbol_test): New.
            Run PR ld/33673 tests.
    
    Signed-off-by: H.J. Lu <hjl.tools@gmail.com>
Comment 59 Sourceware Commits 2026-01-24 07:04:21 UTC
The master branch has been updated by Rainer Orth <ro@sourceware.org>:

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=87997f8b4f5d348557fba410c966bd72d3670c1a

commit 87997f8b4f5d348557fba410c966bd72d3670c1a
Author: Rainer Orth <ro@CeBiTec.Uni-Bielefeld.DE>
Date:   Sat Jan 24 08:02:14 2026 +0100

    ld: testsuite: Skip pr33577 tests with GNU extensions on Solaris [PR33577]
    
    Several of the ld-elfvers pr33577 tests FAIL on Solaris, for either or
    both of two reasons:
    
    * Tests using ld --hash-style=gnu cannot work on Solaris:
      .gnu.hash/SHT_GNU_HASH sections are a GNU extension not supported by
      Solaris ld.so.1.
    
    * Similarly, binding different implementations of the same symbol to
      different symbol versions is a GNU extension that wasn't in the
      original Solaris specification of symbol versioning.  ld.so.1 doesn't
      support it and never will.
    
      This can be seen in the elfdump output for the .dynsym section:
    
    Symbol Table Section:  .dynsym
      index     value size  type bind oth ver shndx         name
    
        [8]     0x630  0xd  FUNC GLOB  D   1H .text         foo
       [10]     0x620  0x6  FUNC GLOB  D    2 .text         foo
    
      foo is bound to both version 1 (the Base version) and version 2 (VERS_1
      from pr33577.map).
    
      Same for .symtab:
    
    Symbol Table Section:  .symtab
      index    value size  type bind oth ver shndx       name
    
       [28]     0x620  0x6  FUNC GLOB  D    0 .text         foo
       [35]     0x630  0xd  FUNC GLOB  D    0 .text         foo@
    
      As I said, ld.so.1 doesn't support <symbol>@<version> (in this case the
      Base version) at all.
    
    Therefore the tests that employ those extensions are guarded with
    supports_gnu_osabi.
    
    Tested on sparc{,v9}-sun-solaris2.11, sparc{,64}-unknown-linux-gnu,
    {i386,amd64}-pc-solaris2.11, and {x86_64,i686}-pc-linux-gnu.
    
    2026-01-23  Rainer Orth  <ro@CeBiTec.Uni-Bielefeld.DE>
    
            ld:
            PR ld/33577
            * testsuite/ld-elfvers/vers.exp (base_symbol_test): Only run
            pr33577a with libpr33577-versioned.so test on ELFOSABI_GNU
            systems.
            Likewise for run base_symbol_tests with --hash-style=gnu.