Keep symbols versioned with .symver preemptible

If an object file defined foo and its alias foo@VER, references to foo
in a shared object were bound locally, although the definition is
exported as foo@VER, so an executable's copy relocation or interposing
definition didn't reach them. Now foo, if not exported itself, shares
foo@VER's dynamic symbol and is as preemptible as foo@VER.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Rui UeyamaandClaude Opus 5.5 committed 2026-10-21 13:18:54 +09:00
1 parent 605749d515
commit 11fa24033d
7 files changed
+145 -1

No files matched your search

+5
View File
@@ -42,6 +42,11 @@ impl<E: Target> DynsymSection<E> {
if self.symbols.is_empty() {
self.symbols.push(None);
}
// A symbol that shares another's dynamic symbol gets that one's index
// (sort_dynsyms()).
if symbols[sym].shares_dynsym() {
return;
}
if symbols[sym].dynsym_idx(symbols).is_none() {
// Mark the symbol as queued before sort_dynsyms assigns its real index.
*symbols.aux_mut(sym).dynsym_idx.get_mut() = u32::MAX - 1;
+7
View File
@@ -261,6 +261,12 @@ pub struct Context<E: Target> {
pub dynamic_list_patterns: Vec<DynamicPattern>,
pub default_version: u16,
// `foo` and `foo@VERSION` that an object file defines as one, with
// `.symver foo, foo@VERSION` (see parse_symbol_version()). Once
// compute_import_export() has run, only those where `foo` shares
// `foo@VERSION`'s dynamic symbol.
pub symver_aliases: Vec<(SymbolId, SymbolId)>,
// For thread-local variables
pub tls_begin: u64,
pub tp_addr: u64,
@@ -351,6 +357,7 @@ impl<E: Target> Context<E> {
version_patterns: Vec::new(),
dynamic_list_patterns: Vec::new(),
default_version: crate::elf::VER_NDX_UNSPECIFIED as u16,
symver_aliases: Vec::new(),
tls_begin: 0,
tp_addr: 0,
dtp_addr: 0,
+1 -1
View File
@@ -508,7 +508,7 @@ unsafe fn get_symbols<E: Target>(
Some(owner) if owner == this => {
if sym.referenced_by_regular_obj() {
LDPR_PREVAILING_DEF
} else if sym.is_exported() {
} else if sym.is_exported() || sym.is_imported() {
if is_v2 { LDPR_PREVAILING_DEF } else { LDPR_PREVAILING_DEF_IRONLY_EXP }
} else {
LDPR_PREVAILING_DEF_IRONLY
+37
View File
@@ -384,6 +384,7 @@ fn clear_symbol<E: Target>(sym: &mut Symbol) {
sym.set_imported(false);
sym.set_exported(false);
sym.set_versioned_default(false);
sym.set_shares_dynsym(false);
}
/// Resets the resolution of every global symbol. Local symbols belong to
@@ -2795,6 +2796,12 @@ pub fn sort_dynsyms<E: Target>(ctx: &mut Context<E>) {
ctx.dynsym.dynstr_entries = dynstr_entries;
ctx.dynsym.symbols[1..].par_iter_mut().zip(syms).for_each(|(slot, id)| *slot = Some(id));
// A symbol that shares another's dynamic symbol takes its index.
for &(id, alias) in &ctx.symver_aliases {
let idx = ctx.symbols[alias].dynsym_idx(&ctx.symbols).unwrap();
*ctx.symbols.aux_mut(id).dynsym_idx.get_mut() = idx;
}
// ELF's symbol table sh_info holds the index of the first global symbol.
ctx.dynsym.hdr.shdr.sh_info.set(num_locals as u32 + 1);
}
@@ -2916,6 +2923,7 @@ pub fn parse_symbol_version<E: Target>(ctx: &mut Context<E>) {
return;
}
let _t = ctx.timer("parse_symbol_version");
ctx.symver_aliases.clear();
let verdefs: HashMap<&[u8], u16> = args
.version_definitions
@@ -2994,6 +3002,16 @@ pub fn parse_symbol_version<E: Target>(ctx: &mut Context<E>) {
{
ctx.symbols[id2].ver_idx = VER_NDX_LOCAL as u16;
}
// If they are one definition, as `.symver foo, foo@VERSION`
// makes them, `foo` may be reached through `foo@VERSION` if it
// is not exported itself (compute_import_export()).
let esyms = &file.base.elf_syms;
if file.shndx_at(i) == file.shndx_at(sym2_idx)
&& esyms[i].st_value() == esyms[sym2_idx].st_value()
{
ctx.symver_aliases.push((id2, id));
}
}
}
}
@@ -3163,6 +3181,25 @@ pub fn compute_import_export<E: Target>(ctx: &mut Context<E>) {
}
});
}
// A `foo` that is not exported may still have its definition exported
// as `foo@VERSION` (parse_symbol_version()). References to `foo` must
// then be as preemptible as those to `foo@VERSION`, so `foo` shares that
// dynamic symbol: the first such one, as in GNU ld and lld. Only the
// pairs that share are kept, for sort_dynsyms().
ctx.symver_aliases.retain(|&(id, alias)| {
let sym = &ctx.symbols[id];
let shares = !sym.is_exported()
&& !sym.shares_dynsym()
&& sym.visibility() == STV_DEFAULT
&& ctx.symbols[alias].is_imported();
if shares {
let sym = &mut ctx.symbols[id];
sym.set_imported(true);
sym.set_shares_dynsym(true);
}
shares
});
}
// Compute the "address-taken" bit for each input section.
+6
View File
@@ -420,6 +420,11 @@ const WRAPPED: u16 = 1 << 7; // for --wrap
// For symbols with a default symbol version, e.g. foo@@VERSION.
const VERSIONED_DEFAULT: u16 = 1 << 8;
// For `foo` that has no dynamic symbol of its own but shares the one of
// `foo@VERSION`, the second name `.symver` gives its definition. See
// compute_import_export().
const SHARES_DYNSYM: u16 = 1 << 9;
// For --gc-sections
const GC_ROOT: u16 = 1 << 10;
@@ -472,6 +477,7 @@ symbol_bits! {
is_traced, set_traced: TRACED;
is_wrapped, set_wrapped: WRAPPED;
is_versioned_default, set_versioned_default: VERSIONED_DEFAULT;
shares_dynsym, set_shares_dynsym: SHARES_DYNSYM;
gc_root, set_gc_root: GC_ROOT;
referenced_by_regular_obj, set_referenced_by_regular_obj: REFERENCED_BY_REGULAR_OBJ;
comdat_claimed_by_ir, set_comdat_claimed_by_ir: COMDAT_CLAIMED_BY_IR;
+25
View File
@@ -18,3 +18,28 @@ $CC -B. -o $t/c.so -shared $t/a.o -Wl,--version-script=$t/b.version
readelf -W --dyn-syms $t/c.so | grep -F 'foo@TEST1'
readelf -W --dyn-syms $t/c.so | grep -F 'foo@@TEST2'
# The directives in the other order put the versions in the other order
# in the symbol table.
cat <<EOF | $CC -o $t/d.o -c -xc - -fPIC
void foo() {}
__asm__(".symver foo, foo@@TEST2");
__asm__(".symver foo, foo@TEST1");
EOF
$CC -B. -o $t/e.so -shared $t/d.o -Wl,--version-script=$t/b.version
readelf -W --dyn-syms $t/e.so | grep -F 'foo@TEST1'
readelf -W --dyn-syms $t/e.so | grep -F 'foo@@TEST2'
# A version script can give the default version instead.
cat <<EOF | $CC -o $t/f.o -c -xc - -fPIC
void foo() {}
__asm__(".symver foo, foo@TEST1");
EOF
echo 'TEST1 {}; TEST2 { foo; };' > $t/g.version
$CC -B. -o $t/h.so -shared $t/f.o -Wl,--version-script=$t/g.version
readelf -W --dyn-syms $t/h.so | grep -F 'foo@TEST1'
readelf -W --dyn-syms $t/h.so | grep -F 'foo@@TEST2'
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
# References to `foo` and to its alias `foo@VER1` bind to the exported
# `foo@VER1`, so that the executable's copy of `foo` and its `bar`
# preempt them.
cat <<EOF | $CC -fPIC -c -o $t/a.o -xc -
int foo = 1;
__asm__(".symver foo, foo@VER1");
int bar() { return 1; }
__asm__(".symver bar, bar@VER1");
int get_foo() { return foo; }
int call_bar() { return bar(); }
EOF
cat <<EOF > $t/b.c
extern int foo_v1;
__asm__(".symver foo_v1, foo@VER1");
int bar_v1();
__asm__(".symver bar_v1, bar@VER1");
int get_foo_v1() { return foo_v1; }
int call_bar_v1() { return bar_v1(); }
EOF
echo 'VER1 {}; VER2 { global: get_*; call_*; local: *; };' > $t/c.ver
cat <<EOF | $CC -fno-PIC -c -o $t/d.o -xc -
#include <stdio.h>
extern int foo;
__asm__(".symver foo, foo@VER1");
int bar() { return 2; }
int get_foo();
int get_foo_v1();
int call_bar();
int call_bar_v1();
int main() {
foo = 42;
printf("%d %d %d %d\n", get_foo(), get_foo_v1(), call_bar(), call_bar_v1());
}
EOF
check() {
$CC -B. -shared -o $t/e.so "$@" -Wl,--version-script=$t/c.ver
$CC -B. -no-pie -Wl,-E -o $t/exe $t/d.o $t/e.so
$QEMU $t/exe | grep '^42 42 2 2$'
}
$CC -fPIC -c -o $t/b.o $t/b.c
check $t/a.o $t/b.o
# After GNU ld's -r, as in glibc's libc_pic.os, the references to
# `foo@VER1` refer to its definition.
if test_cflags -fuse-ld=bfd; then
$CC -fuse-ld=bfd -r -o $t/ab.o $t/a.o $t/b.o
check $t/ab.o
fi
# With LTO, the references to `foo@VER1` appear only after the first
# symbol resolution.
if test_cflags -flto; then
$CC -fPIC -flto -c -o $t/b.o $t/b.c
check -flto $t/a.o $t/b.o
fi