Use .gnu.version_r when registering undefined DSO symbols

For an undefined symbol in a shared library, mold treated the versym
as VER_NDX_GLOBAL and registered the symbol under its bare name. That
meant an undef foo@VER_1.0 in libbar.so was looked up as just foo, so
a non-default foo@VER_1.0 in another DSO did not satisfy it and
--no-allow-shlib-undefined wrongly reported "undefined symbol: foo".

Parse .gnu.version_r and, when an undef has a versym indexing into it,
register the symbol as name@VERSION so the existing versioned-symbol
resolution path picks up matching definitions.

Fixes https://github.com/rui314/mold/issues/1571

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Rui UeyamaandClaude Opus 4.7 committed 2026-05-06 18:29:32 +09:00
1 parent 82b5ed48bf
commit 6b8293bf8c
3 files changed
+84 -5

No files matched your search

+49 -5
View File
@@ -1261,6 +1261,7 @@ void SharedFile<E>::parse(Context<E> &ctx) {
this->symbol_strtab = this->get_string(ctx, symtab_sec->sh_link);
soname = get_soname(ctx);
version_strings = read_verdef(ctx);
append(version_strings, read_verneed(ctx));
// Read a symbol table.
std::span<ElfSym<E>> esyms = this->template get_data<ElfSym<E>>(ctx, *symtab_sec);
@@ -1271,7 +1272,7 @@ void SharedFile<E>::parse(Context<E> &ctx) {
for (i64 i = symtab_sec->sh_info; i < esyms.size(); i++) {
u16 ver;
if (vers.empty() || esyms[i].is_undef())
if (vers.empty())
ver = VER_NDX_GLOBAL;
else
ver = vers[i] & ~VERSYM_HIDDEN;
@@ -1280,10 +1281,17 @@ void SharedFile<E>::parse(Context<E> &ctx) {
continue;
this->elf_syms2.push_back(esyms[i]);
this->versyms.push_back(ver);
// Defined symbols index into verdef; undefined symbols index into
// verneed. We record VER_NDX_GLOBAL for undefined symbols here
// because resolve_symbols only consults versyms[] for defined
// symbols (see SharedFile::resolve_symbols).
this->versyms.push_back(esyms[i].is_undef() ? (u16)VER_NDX_GLOBAL : ver);
std::string_view name = this->symbol_strtab.data() + esyms[i].st_name;
bool has_version = ver != VER_NDX_GLOBAL && ver < version_strings.size() &&
!version_strings[ver].empty();
auto get_versioned_sym = [&] {
std::string_view key = save_string(
ctx, std::string(name) + "@" + std::string(version_strings[ver]));
@@ -1304,12 +1312,13 @@ void SharedFile<E>::parse(Context<E> &ctx) {
// and `foo@VERSION` as usual, but with information to forward
// references to `foo@VERSION` to `foo`. After name resolution, we
// visit all symbol references to redirect `foo@VERSION` to `foo`.
if (vers.empty() || ver == VER_NDX_GLOBAL) {
if (!has_version) {
// Unversioned symbol
this->symbols.push_back(get_symbol(ctx, name));
this->symbols2.push_back(nullptr);
} else if (vers[i] & VERSYM_HIDDEN) {
// Versioned non-default symbol
} else if (esyms[i].is_undef() || (vers[i] & VERSYM_HIDDEN)) {
// Versioned non-default symbol, or undefined reference whose
// version comes from .gnu.version_r.
this->symbols.push_back(get_versioned_sym());
this->symbols2.push_back(nullptr);
} else {
@@ -1404,6 +1413,41 @@ std::vector<std::string_view> SharedFile<E>::read_verdef(Context<E> &ctx) {
return vec;
}
// Reads .gnu.version_r and returns a vector of version names indexed
// by vna_other. .gnu.version_r records the versions a shared object
// needs from the libraries it depends on; the versym of an undefined
// dynamic symbol indexes into these entries.
template <typename E>
std::vector<std::string_view> SharedFile<E>::read_verneed(Context<E> &ctx) {
ElfShdr<E> *verneed_sec = this->find_section(SHT_GNU_VERNEED);
if (!verneed_sec)
return {};
std::vector<std::string_view> vec;
std::string_view verneed = this->get_string(ctx, *verneed_sec);
std::string_view strtab = this->get_string(ctx, verneed_sec->sh_link);
u8 *ptr = (u8 *)verneed.data();
for (;;) {
ElfVerneed<E> *vn = (ElfVerneed<E> *)ptr;
u8 *aux_ptr = ptr + vn->vn_aux;
for (i64 i = 0; i < vn->vn_cnt; i++) {
ElfVernaux<E> *aux = (ElfVernaux<E> *)aux_ptr;
u16 idx = aux->vna_other & ~VERSYM_HIDDEN;
if (vec.size() <= idx)
vec.resize(idx + 1);
vec[idx] = strtab.data() + aux->vna_name;
if (!aux->vna_next)
break;
aux_ptr += aux->vna_next;
}
if (!vn->vn_next)
break;
ptr += vn->vn_next;
}
return vec;
}
template <typename E>
void SharedFile<E>::resolve_symbols(Context<E> &ctx) {
for (i64 i = 0; i < this->symbols.size(); i++) {
+1
View File
@@ -1851,6 +1851,7 @@ private:
void maybe_override_symbol(Symbol<E> &sym, const ElfSym<E> &esym);
std::vector<std::string_view> read_dt_needed(Context<E> &ctx);
std::vector<std::string_view> read_verdef(Context<E> &ctx);
std::vector<std::string_view> read_verneed(Context<E> &ctx);
std::vector<u16> versyms;
const ElfShdr<E> *symtab_sec;
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
. $(dirname $0)/common.inc
[ "$(uname)" = FreeBSD ] && skip
# libfoo.so defines foo@VER_1.0 as a non-default version. Referencing
# the bare name `foo` does NOT bind to this symbol.
cat <<EOF > $t/libfoo.ver
VER_1.0 {};
EOF
cat <<EOF | $CC -B. -shared -fPIC -o $t/libfoo.so -xc - \
-Wl,--version-script=$t/libfoo.ver
__asm__(".symver foo_impl,foo@VER_1.0");
int foo_impl(void) { return 42; }
EOF
# libbar.so references foo@VER_1.0 via .symver. Its dynsym holds an
# undefined symbol with name "foo" whose versym indexes into the
# verneed (.gnu.version_r) entry naming "VER_1.0".
cat <<EOF | $CC -B. -shared -fPIC -o $t/libbar.so -xc - -L$t -lfoo
__asm__(".symver foo_v1,foo@VER_1.0");
int foo_v1(void);
int call_foo_v1(void) { return foo_v1(); }
EOF
cat <<EOF | $CC -c -o $t/a.o -xc -
int call_foo_v1(void);
int main(void) { return call_foo_v1(); }
EOF
# Both libs are present; libbar.so's undef foo@VER_1.0 is satisfied by
# libfoo.so. --no-allow-shlib-undefined must accept the link.
$CC -B. -o $t/exe $t/a.o -Wl,--no-allow-shlib-undefined -L$t -lbar -lfoo