From 7ac49d1ba60ae1ea33d4fe43a42d3a7be1310ea9 Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Wed, 15 Apr 2026 19:17:33 +0800 Subject: [PATCH 1/5] staticlib symbol trimming: hide non-exported symbols in staticlibs --- .../rustc_codegen_ssa/src/back/archive.rs | 172 +++++++++++++++++- compiler/rustc_codegen_ssa/src/back/link.rs | 18 ++ compiler/rustc_codegen_ssa/src/errors.rs | 6 + compiler/rustc_interface/src/tests.rs | 1 + compiler/rustc_session/src/config.rs | 8 + compiler/rustc_session/src/options.rs | 2 + .../staticlib-hide-internal-symbols.md | 11 ++ .../staticlib-hide-internal-symbols/lib.rs | 40 ++++ .../staticlib-hide-internal-symbols/main.c | 18 ++ .../staticlib-hide-internal-symbols/rmake.rs | 133 ++++++++++++++ .../wrong-crate-type.rs | 7 + .../wrong-crate-type.stderr | 2 + 12 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md create mode 100644 tests/run-make/staticlib-hide-internal-symbols/lib.rs create mode 100644 tests/run-make/staticlib-hide-internal-symbols/main.c create mode 100644 tests/run-make/staticlib-hide-internal-symbols/rmake.rs create mode 100644 tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs create mode 100644 tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr diff --git a/compiler/rustc_codegen_ssa/src/back/archive.rs b/compiler/rustc_codegen_ssa/src/back/archive.rs index 3f12e857391b2..1f538d9c9816e 100644 --- a/compiler/rustc_codegen_ssa/src/back/archive.rs +++ b/compiler/rustc_codegen_ssa/src/back/archive.rs @@ -11,7 +11,7 @@ use ar_archive_writer::{ pub use ar_archive_writer::{DEFAULT_OBJECT_READER, ObjectReader}; use object::read::archive::ArchiveFile; use object::read::macho::FatArch; -use rustc_data_structures::fx::FxIndexSet; +use rustc_data_structures::fx::{FxHashSet, FxIndexSet}; use rustc_data_structures::memmap::Mmap; use rustc_fs_util::TempDirBuilder; use rustc_metadata::EncodedMetadata; @@ -318,6 +318,8 @@ pub trait ArchiveBuilder { ) -> io::Result<()>; fn build(self: Box, output: &Path) -> bool; + + fn set_keep_symbols(&mut self, keep: FxHashSet); } pub struct ArArchiveBuilderBuilder; @@ -337,6 +339,7 @@ pub struct ArArchiveBuilder<'a> { // Don't use an `HashMap` here, as the order is important. `lib.rmeta` needs // to be at the end of an archive in some cases for linkers to not get confused. entries: Vec<(Vec, ArchiveEntry)>, + keep_symbols: Option>, } #[derive(Debug)] @@ -347,7 +350,17 @@ enum ArchiveEntry { impl<'a> ArArchiveBuilder<'a> { pub fn new(sess: &'a Session, object_reader: &'static ObjectReader) -> ArArchiveBuilder<'a> { - ArArchiveBuilder { sess, object_reader, src_archives: vec![], entries: vec![] } + ArArchiveBuilder { + sess, + object_reader, + src_archives: vec![], + entries: vec![], + keep_symbols: None, + } + } + + pub fn set_keep_symbols(&mut self, keep: FxHashSet) { + self.keep_symbols = Some(keep); } } @@ -460,6 +473,10 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> { } } } + + fn set_keep_symbols(&mut self, keep: FxHashSet) { + self.keep_symbols = Some(keep); + } } impl<'a> ArArchiveBuilder<'a> { @@ -478,7 +495,7 @@ impl<'a> ArArchiveBuilder<'a> { let mut entries = Vec::new(); for (entry_name, entry) in self.entries { - let data = + let data: Box> = match entry { ArchiveEntry::FromArchive { archive_index, file_range } => { let src_archive = &self.src_archives[archive_index]; @@ -498,6 +515,16 @@ impl<'a> ArArchiveBuilder<'a> { }, }; + let data: Box> = if let Some(ref keep) = self.keep_symbols { + if let Some(filtered) = elf_filter_global_symbols(data.as_ref().as_ref(), keep) { + Box::new(filtered) + } else { + data + } + } else { + data + }; + entries.push(NewArchiveMember { buf: data, object_reader: self.object_reader, @@ -557,3 +584,142 @@ impl<'a> ArArchiveBuilder<'a> { fn io_error_context(context: &str, err: io::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, format!("{context}: {err}")) } + +/// For ELF object files, set `STV_HIDDEN` visibility on GLOBAL/WEAK symbols +/// that are NOT in the `keep_symbols` set. Returns `Some(modified_data)` if +/// any changes were made, `None` if the data is not an ELF object or no +/// changes were needed. +fn elf_filter_global_symbols(data: &[u8], keep_symbols: &FxHashSet) -> Option> { + use object::{Endianness, elf}; + + if data.len() < 16 || &data[0..4] != elf::ELFMAG { + return None; + } + + match data[4] { + elf::ELFCLASS64 => elf_filter_symbols_inner::>( + data, + keep_symbols, + /* sym_entry_size= */ 24, + /* st_info_offset= */ 4, + /* st_other_offset= */ 5, + ), + elf::ELFCLASS32 => elf_filter_symbols_inner::>( + data, + keep_symbols, + /* sym_entry_size= */ 16, + /* st_info_offset= */ 12, + /* st_other_offset= */ 13, + ), + _ => None, + } +} + +fn elf_filter_symbols_inner>( + data: &[u8], + keep_symbols: &FxHashSet, + sym_entry_size: usize, + st_info_offset: usize, + st_other_offset: usize, +) -> Option> +where + u64: From, +{ + use object::read::elf::SectionHeader; + use object::{Endianness, elf}; + + let endian = match Elf::parse(data) { + Ok(h) => match h.endian() { + Ok(e) => e, + Err(_) => return None, + }, + Err(_) => return None, + }; + + let header = Elf::parse(data).unwrap(); + let sections = match header.sections(endian, data) { + Ok(s) => s, + Err(_) => return None, + }; + + let mut modified: Option> = None; + + for section in sections.iter() { + if section.sh_type(endian) != elf::SHT_SYMTAB { + continue; + } + + let strtab_index = section.sh_link(endian) as usize; + let strtab_section = match sections.section(object::SectionIndex(strtab_index)) { + Ok(s) => s, + Err(_) => continue, + }; + let strtab_data = match strtab_section.data(endian, data) { + Ok(d) => d, + Err(_) => continue, + }; + + if sym_entry_size == 0 { + continue; + } + + let sym_offset = u64::from(section.sh_offset(endian)) as usize; + let sym_size = u64::from(section.sh_size(endian)) as usize; + let sym_count = sym_size / sym_entry_size; + + let buf = modified.get_or_insert_with(|| data.to_vec()); + + for i in 1..sym_count { + let off = sym_offset + i * sym_entry_size; + if off + sym_entry_size > buf.len() { + break; + } + + let st_info = buf[off + st_info_offset]; + let binding = st_info >> 4; + + // Only process GLOBAL and WEAK symbols + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + + let st_shndx_offset = st_other_offset + 1; + let st_shndx_bytes: [u8; 2] = + buf[off + st_shndx_offset..off + st_shndx_offset + 2].try_into().unwrap_or([0, 0]); + let st_shndx = match endian { + Endianness::Little => u16::from_le_bytes(st_shndx_bytes), + Endianness::Big => u16::from_be_bytes(st_shndx_bytes), + }; + if st_shndx == elf::SHN_UNDEF as u16 { + continue; + } + + let st_name_bytes: [u8; 4] = buf[off..off + 4].try_into().unwrap_or([0; 4]); + let st_name_off = match endian { + Endianness::Little => u32::from_le_bytes(st_name_bytes), + Endianness::Big => u32::from_be_bytes(st_name_bytes), + } as usize; + + if st_name_off >= strtab_data.len() { + continue; + } + let name_end = strtab_data[st_name_off..] + .iter() + .position(|&b| b == 0) + .unwrap_or(strtab_data.len() - st_name_off); + let name = match std::str::from_utf8(&strtab_data[st_name_off..st_name_off + name_end]) + { + Ok(s) => s, + Err(_) => continue, + }; + + if keep_symbols.contains(name) { + continue; + } + + buf[off + st_other_offset] = elf::STV_HIDDEN; + } + } + + modified +} diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index cb22aac4e952d..7eb5674662820 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -525,6 +525,24 @@ fn link_staticlib( sess.dcx().emit_fatal(e); } + if sess.opts.unstable_opts.staticlib_hide_internal_symbols { + if !matches!(&*sess.target.archive_format, "gnu" | "bsd") { + sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported { + archive_format: sess.target.archive_format.to_string(), + }); + } else if let Some(symbols) = + codegen_results.crate_info.exported_symbols.get(&CrateType::Staticlib) + { + use rustc_data_structures::fx::FxHashSet; + let mut keep: FxHashSet = symbols.iter().map(|(s, _)| s.clone()).collect(); + // rust_eh_personality cannot be mangled and hidden. + // See discussions in https://github.com/rust-lang/rust/issues/104707 + // Keep it visible so that .eh_frame references from other object files + keep.insert("rust_eh_personality".to_owned()); + ab.set_keep_symbols(keep); + } + } + ab.build(out_filename); let crates = crate_info.used_crates.iter(); diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs index 8a97521feb436..17926bdfb95d2 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -675,6 +675,12 @@ pub(crate) struct UnknownArchiveKind<'a> { #[diag("linking static libraries is not supported for BPF")] pub(crate) struct BpfStaticlibNotSupported; +#[derive(Diagnostic)] +#[diag("-Zstaticlib-hide-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`")] +pub(crate) struct StaticlibHideInternalSymbolsUnsupported { + pub archive_format: String, +} + #[derive(Diagnostic)] #[diag("entry symbol `main` declared multiple times")] #[help( diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index 417cde119c21f..bb5e63368ea9b 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -869,6 +869,7 @@ fn test_unstable_options_tracking_hash() { tracked!(split_lto_unit, Some(true)); tracked!(src_hash_algorithm, Some(SourceFileHashAlgorithm::Sha1)); tracked!(stack_protector, StackProtector::All); + tracked!(staticlib_hide_internal_symbols, true); tracked!(teach, true); tracked!(thinlto, Some(true)); tracked!(tiny_const_eval_limit, true); diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 3c0b3b4876659..b46a80e91384a 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -2473,6 +2473,14 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let mut collected_options = Default::default(); let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options); + + if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::Staticlib) + { + early_dcx.early_fatal( + "-Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib`", + ); + } + let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(early_dcx, matches); if !unstable_opts.unstable_options && json_timings { diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 9fc6036b98b34..f428cd86912d9 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2764,6 +2764,8 @@ written to standard error output)"), "control stack smash protection strategy (`rustc --print stack-protector-strategies` for details)"), staticlib_allow_rdylib_deps: bool = (false, parse_bool, [TRACKED], "allow staticlibs to have rust dylib dependencies"), + staticlib_hide_internal_symbols: bool = (false, parse_bool, [TRACKED], + "hide Rust internal symbols when building staticlibs"), staticlib_prefer_dynamic: bool = (false, parse_bool, [TRACKED], "prefer dynamic linking to static linking for staticlibs (default: no)"), strict_init_checks: bool = (false, parse_bool, [TRACKED], diff --git a/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md new file mode 100644 index 0000000000000..603a5f96ab696 --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md @@ -0,0 +1,11 @@ +# `staticlib-hide-internal-symbols` + +When building a `staticlib`, this option hides all Rust-internal symbols +(except `rust_eh_personality`) by setting their ELF visibility to +`STV_HIDDEN`. + +This option can only be used with `--crate-type staticlib`. Using it with +other crate types will result in a compilation error. + +Currently only ELF targets are supported (Linux, BSD, etc.). On non-ELF +targets (macOS, Windows), a warning is emitted and the flag has no effect. diff --git a/tests/run-make/staticlib-hide-internal-symbols/lib.rs b/tests/run-make/staticlib-hide-internal-symbols/lib.rs new file mode 100644 index 0000000000000..4bbf21bf1918a --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols/lib.rs @@ -0,0 +1,40 @@ +#![crate_type = "staticlib"] + +use std::collections::HashMap; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +#[no_mangle] +pub extern "C" fn my_add(a: i32, b: i32) -> i32 { + a + b +} + +#[no_mangle] +pub extern "C" fn my_hash_lookup(key: u64) -> u64 { + let mut map = HashMap::new(); + for i in 0..100u64 { + map.insert(i, i.wrapping_mul(2654435761)); + } + *map.get(&key).unwrap_or(&0) +} + +fn internal_helper() -> i32 { + 42 +} + +#[no_mangle] +pub extern "C" fn call_internal() -> i32 { + internal_helper() +} + +#[no_mangle] +pub extern "C" fn my_safe_div(a: i32, b: i32) -> i32 { + match catch_unwind(AssertUnwindSafe(|| { + if b == 0 { + panic!("division by zero!"); + } + a / b + })) { + Ok(result) => result, + Err(_) => -1, + } +} diff --git a/tests/run-make/staticlib-hide-internal-symbols/main.c b/tests/run-make/staticlib-hide-internal-symbols/main.c new file mode 100644 index 0000000000000..580c805fed158 --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols/main.c @@ -0,0 +1,18 @@ +extern int my_add(int a, int b); +extern unsigned long my_hash_lookup(unsigned long key); +extern int call_internal(void); +extern int my_safe_div(int a, int b); + +int main() { + if (my_add(10, 20) != 30) + return 1; + if (my_hash_lookup(5) != 5UL * 2654435761UL) + return 1; + if (call_internal() != 42) + return 1; + if (my_safe_div(100, 5) != 20) + return 1; + if (my_safe_div(100, 0) != -1) + return 1; + return 0; +} diff --git a/tests/run-make/staticlib-hide-internal-symbols/rmake.rs b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs new file mode 100644 index 0000000000000..0a11ce84f785b --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs @@ -0,0 +1,133 @@ +//@ only-elf +//@ ignore-cross-compile + +use std::collections::HashSet; + +use run_make_support::object::Endianness; +use run_make_support::object::read::archive::ArchiveFile; +use run_make_support::object::read::elf::{FileHeader as _, SectionHeader as _, Sym as _}; +use run_make_support::{cc, extra_c_flags, object, rfs, run, rustc, static_lib_name}; + +type FileHeader64 = run_make_support::object::elf::FileHeader64; +type SymbolTable<'data> = run_make_support::object::read::elf::SymbolTable<'data, FileHeader64>; + +const EXPORTED: &[&str] = + &["my_add", "my_hash_lookup", "call_internal", "my_safe_div", "rust_eh_personality"]; + +fn main() { + let lib_name = static_lib_name("lib"); + + rustc() + .input("lib.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-hide-internal-symbols") + .opt() + .run(); + + cc().input("main.c").input(&lib_name).out_exe("main").args(extra_c_flags()).run(); + run("main"); + + let data = rfs::read(&lib_name); + check_symbols(&data, true); + + rfs::remove_file(&lib_name); + rustc().input("lib.rs").crate_type("staticlib").opt().run(); + + let data = rfs::read(&lib_name); + check_symbols(&data, false); +} + +fn check_symbols(archive_data: &[u8], with_flag: bool) { + let archive = ArchiveFile::parse(archive_data).unwrap(); + let mut found_exported = HashSet::new(); + + for member in archive.members() { + let member = member.unwrap(); + let member_name = std::str::from_utf8(member.name()).unwrap(); + if !member_name.ends_with(".rcgu.o") { + continue; + } + let data = member.data(archive_data).unwrap(); + + let Ok(header) = FileHeader64::parse(data) else { continue }; + let Ok(endian) = header.endian() else { continue }; + let Ok(sections) = header.sections(endian, data) else { continue }; + + for (si, section) in sections.enumerate() { + if section.sh_type(endian) != object::elf::SHT_SYMTAB { + continue; + } + let Ok(symbols) = SymbolTable::parse(endian, data, §ions, si, section) else { + continue; + }; + let strtab = symbols.strings(); + + for symbol in symbols.symbols() { + let vis = symbol.st_visibility(); + let bind = symbol.st_bind(); + let shndx = symbol.st_shndx(endian); + + if shndx == object::elf::SHN_UNDEF as u16 { + continue; + } + if bind != object::elf::STB_GLOBAL && bind != object::elf::STB_WEAK { + continue; + } + + let Some(name) = read_symbol_name(endian, symbol, &strtab) else { continue }; + + let exported = EXPORTED.contains(&name); + + if with_flag { + let expected = + if exported { object::elf::STV_DEFAULT } else { object::elf::STV_HIDDEN }; + assert_eq!( + vis, + expected, + "with -Z: `{name}` should be {}, got {}", + visibility_name(expected), + visibility_name(vis) + ); + } else if exported { + assert_eq!( + vis, + object::elf::STV_DEFAULT, + "without -Z: `{name}` should be STV_DEFAULT, got {}", + visibility_name(vis) + ); + } + + if exported { + found_exported.insert(name.to_string()); + } + } + } + } + + for expected in EXPORTED { + assert!( + found_exported.contains(*expected), + "expected to find exported symbol `{expected}` in archive" + ); + } +} + +fn read_symbol_name<'data>( + endian: Endianness, + symbol: &run_make_support::object::elf::Sym64, + strtab: &object::StringTable<'data>, +) -> Option<&'data str> { + let bytes = strtab.get(symbol.st_name(endian)).ok()?; + let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + std::str::from_utf8(&bytes[..end]).ok() +} + +fn visibility_name(v: u8) -> &'static str { + match v { + v if v == object::elf::STV_DEFAULT => "STV_DEFAULT", + v if v == object::elf::STV_HIDDEN => "STV_HIDDEN", + v if v == object::elf::STV_INTERNAL => "STV_INTERNAL", + v if v == object::elf::STV_PROTECTED => "STV_PROTECTED", + _ => "UNKNOWN", + } +} diff --git a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs new file mode 100644 index 0000000000000..06663de81072d --- /dev/null +++ b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs @@ -0,0 +1,7 @@ +//@ compile-flags: -Zstaticlib-hide-internal-symbols --crate-type bin + +#![feature(no_core)] +#![no_core] +#![no_main] + +//~? ERROR can only be used with `--crate-type staticlib` diff --git a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr new file mode 100644 index 0000000000000..999517b2d88d4 --- /dev/null +++ b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr @@ -0,0 +1,2 @@ +error: -Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib` + From 5824be807daba5597c59cb20cc68816da19391e5 Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Thu, 16 Apr 2026 10:37:11 +0800 Subject: [PATCH 2/5] update --- compiler/rustc_codegen_ssa/src/back/link.rs | 10 ++-------- compiler/rustc_codegen_ssa/src/errors.rs | 4 +++- compiler/rustc_session/src/config.rs | 2 +- .../compiler-flags/staticlib-hide-internal-symbols.md | 3 +-- .../run-make/staticlib-hide-internal-symbols/rmake.rs | 3 +-- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index 7eb5674662820..65bbd12620759 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -530,15 +530,9 @@ fn link_staticlib( sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported { archive_format: sess.target.archive_format.to_string(), }); - } else if let Some(symbols) = - codegen_results.crate_info.exported_symbols.get(&CrateType::Staticlib) - { + } else if let Some(symbols) = crate_info.exported_symbols.get(&CrateType::StaticLib) { use rustc_data_structures::fx::FxHashSet; - let mut keep: FxHashSet = symbols.iter().map(|(s, _)| s.clone()).collect(); - // rust_eh_personality cannot be mangled and hidden. - // See discussions in https://github.com/rust-lang/rust/issues/104707 - // Keep it visible so that .eh_frame references from other object files - keep.insert("rust_eh_personality".to_owned()); + let keep: FxHashSet = symbols.iter().map(|(s, _)| s.clone()).collect(); ab.set_keep_symbols(keep); } } diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs index 17926bdfb95d2..84a53c1ed28ae 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -676,7 +676,9 @@ pub(crate) struct UnknownArchiveKind<'a> { pub(crate) struct BpfStaticlibNotSupported; #[derive(Diagnostic)] -#[diag("-Zstaticlib-hide-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`")] +#[diag( + "-Zstaticlib-hide-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`" +)] pub(crate) struct StaticlibHideInternalSymbolsUnsupported { pub archive_format: String, } diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index b46a80e91384a..48fc67f1e632f 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -2474,7 +2474,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options); - if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::Staticlib) + if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::StaticLib) { early_dcx.early_fatal( "-Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib`", diff --git a/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md index 603a5f96ab696..1896e23f42147 100644 --- a/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md @@ -1,8 +1,7 @@ # `staticlib-hide-internal-symbols` When building a `staticlib`, this option hides all Rust-internal symbols -(except `rust_eh_personality`) by setting their ELF visibility to -`STV_HIDDEN`. +by setting their ELF visibility to `STV_HIDDEN`. This option can only be used with `--crate-type staticlib`. Using it with other crate types will result in a compilation error. diff --git a/tests/run-make/staticlib-hide-internal-symbols/rmake.rs b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs index 0a11ce84f785b..570d483642dd1 100644 --- a/tests/run-make/staticlib-hide-internal-symbols/rmake.rs +++ b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs @@ -11,8 +11,7 @@ use run_make_support::{cc, extra_c_flags, object, rfs, run, rustc, static_lib_na type FileHeader64 = run_make_support::object::elf::FileHeader64; type SymbolTable<'data> = run_make_support::object::read::elf::SymbolTable<'data, FileHeader64>; -const EXPORTED: &[&str] = - &["my_add", "my_hash_lookup", "call_internal", "my_safe_div", "rust_eh_personality"]; +const EXPORTED: &[&str] = &["my_add", "my_hash_lookup", "call_internal", "my_safe_div"]; fn main() { let lib_name = static_lib_name("lib"); From c7d4e98b4690d182a123e369696b6ca2ee1ca07c Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Thu, 16 Apr 2026 11:03:53 +0800 Subject: [PATCH 3/5] update --- tests/ui/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ui/README.md b/tests/ui/README.md index 9ef331698d2b6..e0775a3a04cb8 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1308,6 +1308,10 @@ See [Tracking Issue for stabilizing stack smashing protection (i.e., `-Z stack-p Tests on static items. +## `tests/ui/staticlib-hide-internal-symbols/`: `-Zstaticlib-hide-internal-symbols` command line flag + +Tests for the `-Zstaticlib-hide-internal-symbols` flag, which hides non-exported symbols in ELF static libraries. + ## `tests/ui/statics/` **FIXME**: should probably be merged with `tests/ui/static/`. From 27ab9e5f35b8191745d1dc50394db030fd106cd4 Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Thu, 16 Apr 2026 20:43:08 +0800 Subject: [PATCH 4/5] staticlib symbol renaming: add -Zstaticlib-rename-internal-symbols --- .../rustc_codegen_ssa/src/back/archive.rs | 400 +++++++++++++----- compiler/rustc_codegen_ssa/src/back/link.rs | 14 +- compiler/rustc_codegen_ssa/src/errors.rs | 4 +- compiler/rustc_interface/src/tests.rs | 2 +- compiler/rustc_session/src/config.rs | 5 +- compiler/rustc_session/src/options.rs | 4 +- .../staticlib-hide-internal-symbols.md | 10 - .../staticlib-rename-internal-symbols.md | 16 + .../lib.rs | 0 .../main.c | 0 .../rmake.rs | 4 +- .../dual_main.c | 14 + .../staticlib-rename-internal-symbols/lib.rs | 40 ++ .../staticlib-rename-internal-symbols/liba.rs | 17 + .../staticlib-rename-internal-symbols/libb.rs | 15 + .../staticlib-rename-internal-symbols/main.c | 18 + .../rmake.rs | 172 ++++++++ tests/ui/README.md | 4 +- .../wrong-crate-type.stderr | 2 - .../wrong-crate-type.rs | 2 +- .../wrong-crate-type.stderr | 2 + 21 files changed, 623 insertions(+), 122 deletions(-) delete mode 100644 src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md create mode 100644 src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md rename tests/run-make/{staticlib-hide-internal-symbols => staticlib-rename-internal-symbols-visibility}/lib.rs (100%) rename tests/run-make/{staticlib-hide-internal-symbols => staticlib-rename-internal-symbols-visibility}/main.c (100%) rename tests/run-make/{staticlib-hide-internal-symbols => staticlib-rename-internal-symbols-visibility}/rmake.rs (97%) create mode 100644 tests/run-make/staticlib-rename-internal-symbols/dual_main.c create mode 100644 tests/run-make/staticlib-rename-internal-symbols/lib.rs create mode 100644 tests/run-make/staticlib-rename-internal-symbols/liba.rs create mode 100644 tests/run-make/staticlib-rename-internal-symbols/libb.rs create mode 100644 tests/run-make/staticlib-rename-internal-symbols/main.c create mode 100644 tests/run-make/staticlib-rename-internal-symbols/rmake.rs delete mode 100644 tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr rename tests/ui/{staticlib-hide-internal-symbols => staticlib-rename-internal-symbols}/wrong-crate-type.rs (59%) create mode 100644 tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.stderr diff --git a/compiler/rustc_codegen_ssa/src/back/archive.rs b/compiler/rustc_codegen_ssa/src/back/archive.rs index 1f538d9c9816e..94c5d6e51c43b 100644 --- a/compiler/rustc_codegen_ssa/src/back/archive.rs +++ b/compiler/rustc_codegen_ssa/src/back/archive.rs @@ -9,9 +9,10 @@ use ar_archive_writer::{ ArchiveKind, COFFShortExport, MachineTypes, NewArchiveMember, write_archive_to_stream, }; pub use ar_archive_writer::{DEFAULT_OBJECT_READER, ObjectReader}; +use object::Endianness; use object::read::archive::ArchiveFile; use object::read::macho::FatArch; -use rustc_data_structures::fx::{FxHashSet, FxIndexSet}; +use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet}; use rustc_data_structures::memmap::Mmap; use rustc_fs_util::TempDirBuilder; use rustc_metadata::EncodedMetadata; @@ -319,7 +320,7 @@ pub trait ArchiveBuilder { fn build(self: Box, output: &Path) -> bool; - fn set_keep_symbols(&mut self, keep: FxHashSet); + fn set_rename_symbols(&mut self, keep: FxHashSet, suffix: String); } pub struct ArArchiveBuilderBuilder; @@ -339,7 +340,7 @@ pub struct ArArchiveBuilder<'a> { // Don't use an `HashMap` here, as the order is important. `lib.rmeta` needs // to be at the end of an archive in some cases for linkers to not get confused. entries: Vec<(Vec, ArchiveEntry)>, - keep_symbols: Option>, + rename_symbols: Option<(FxHashSet, String)>, } #[derive(Debug)] @@ -355,12 +356,12 @@ impl<'a> ArArchiveBuilder<'a> { object_reader, src_archives: vec![], entries: vec![], - keep_symbols: None, + rename_symbols: None, } } - pub fn set_keep_symbols(&mut self, keep: FxHashSet) { - self.keep_symbols = Some(keep); + pub fn set_rename_symbols(&mut self, keep: FxHashSet, suffix: String) { + self.rename_symbols = Some((keep, suffix)); } } @@ -474,8 +475,8 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> { } } - fn set_keep_symbols(&mut self, keep: FxHashSet) { - self.keep_symbols = Some(keep); + fn set_rename_symbols(&mut self, keep: FxHashSet, suffix: String) { + self.rename_symbols = Some((keep, suffix)); } } @@ -494,6 +495,38 @@ impl<'a> ArArchiveBuilder<'a> { let mut entries = Vec::new(); + // When renaming symbols, we need a global two-pass approach: + // Pass 1: collect all non-exported defined symbol names across ALL .o files + // Pass 2: rename those symbols (both definitions and undefined references) in each .o file + // This ensures cross-object-file references remain consistent. + let global_rename_set: Option<(FxHashSet, &String)> = + self.rename_symbols.as_ref().map(|(keep, suffix)| { + let mut all_names: FxHashSet = FxHashSet::default(); + for (_, entry) in &self.entries { + let data: Option>> = match entry { + ArchiveEntry::FromArchive { archive_index, file_range } => { + let src_archive = &self.src_archives[*archive_index]; + let d = &src_archive.1[file_range.0 as usize + ..file_range.0 as usize + file_range.1 as usize]; + Some(Box::new(d) as Box>) + } + ArchiveEntry::File(file) => { + let file = File::open(file); + match file { + Ok(f) => unsafe { + Mmap::map(f).ok().map(|m| Box::new(m) as Box>) + }, + Err(_) => None, + } + } + }; + if let Some(data) = data { + elf_collect_rename_set(data.as_ref().as_ref(), keep, &mut all_names); + } + } + (all_names, suffix) + }); + for (entry_name, entry) in self.entries { let data: Box> = match entry { @@ -515,9 +548,12 @@ impl<'a> ArArchiveBuilder<'a> { }, }; - let data: Box> = if let Some(ref keep) = self.keep_symbols { - if let Some(filtered) = elf_filter_global_symbols(data.as_ref().as_ref(), keep) { - Box::new(filtered) + let data: Box> = if let Some((ref rename_set, suffix)) = + global_rename_set + { + if let Some(renamed) = elf_apply_rename(data.as_ref().as_ref(), rename_set, suffix) + { + Box::new(renamed) } else { data } @@ -585,48 +621,87 @@ fn io_error_context(context: &str, err: io::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, format!("{context}: {err}")) } -/// For ELF object files, set `STV_HIDDEN` visibility on GLOBAL/WEAK symbols -/// that are NOT in the `keep_symbols` set. Returns `Some(modified_data)` if -/// any changes were made, `None` if the data is not an ELF object or no -/// changes were needed. -fn elf_filter_global_symbols(data: &[u8], keep_symbols: &FxHashSet) -> Option> { - use object::{Endianness, elf}; +/// Layout constants that differ between ELF32 and ELF64 symbol table entries. +struct ElfSymLayout { + sym_entry_size: usize, + st_info_offset: usize, + st_other_offset: usize, + is_64: bool, +} - if data.len() < 16 || &data[0..4] != elf::ELFMAG { - return None; +impl ElfSymLayout { + const ELF64: ElfSymLayout = + ElfSymLayout { sym_entry_size: 24, st_info_offset: 4, st_other_offset: 5, is_64: true }; + const ELF32: ElfSymLayout = + ElfSymLayout { sym_entry_size: 16, st_info_offset: 12, st_other_offset: 13, is_64: false }; +} + +/// Parsed ELF symbol table information, ready for symbol iteration. +struct ElfSymtab<'a> { + endian: Endianness, + sym_offset: usize, + sym_count: usize, + strtab_data: &'a [u8], + layout: &'static ElfSymLayout, + /// File offset of section header table. + e_shoff: usize, + /// Number of section headers. + e_shnum: usize, + /// Section header index of the strtab linked to the symtab. + strtab_section_index: usize, +} + +impl<'a> ElfSymtab<'a> { + fn sym_off(&self, i: usize) -> usize { + self.sym_offset + i * self.layout.sym_entry_size } - match data[4] { - elf::ELFCLASS64 => elf_filter_symbols_inner::>( - data, - keep_symbols, - /* sym_entry_size= */ 24, - /* st_info_offset= */ 4, - /* st_other_offset= */ 5, - ), - elf::ELFCLASS32 => elf_filter_symbols_inner::>( - data, - keep_symbols, - /* sym_entry_size= */ 16, - /* st_info_offset= */ 12, - /* st_other_offset= */ 13, - ), - _ => None, + fn binding(&self, data: &[u8], i: usize) -> u8 { + let off = self.sym_off(i); + data[off + self.layout.st_info_offset] >> 4 + } + + fn is_defined(&self, data: &[u8], i: usize) -> bool { + use object::elf; + let off = self.sym_off(i) + self.layout.st_other_offset + 1; + let bytes: [u8; 2] = data[off..off + 2].try_into().unwrap_or([0, 0]); + let shndx = match self.endian { + Endianness::Little => u16::from_le_bytes(bytes), + Endianness::Big => u16::from_be_bytes(bytes), + }; + shndx != elf::SHN_UNDEF as u16 + } + + /// Read the symbol name from the linked strtab. + fn read_name(&self, data: &[u8], i: usize) -> Option { + let off = self.sym_off(i); + let name_bytes: [u8; 4] = data[off..off + 4].try_into().ok()?; + let name_off: usize = match self.endian { + Endianness::Little => u32::from_le_bytes(name_bytes), + Endianness::Big => u32::from_be_bytes(name_bytes), + } as usize; + if name_off >= self.strtab_data.len() { + return None; + } + let end = self.strtab_data[name_off..] + .iter() + .position(|&b| b == 0) + .unwrap_or(self.strtab_data.len() - name_off); + let name = std::str::from_utf8(&self.strtab_data[name_off..name_off + end]).ok()?; + Some(name.to_string()) } } -fn elf_filter_symbols_inner>( - data: &[u8], - keep_symbols: &FxHashSet, - sym_entry_size: usize, - st_info_offset: usize, - st_other_offset: usize, -) -> Option> +/// Internal helper: parse ELF symtab using the generic `FileHeader` API. +fn elf_parse_symtab<'data, Elf: object::read::elf::FileHeader>( + data: &'data [u8], + layout: &'static ElfSymLayout, +) -> Option> where u64: From, { - use object::read::elf::SectionHeader; - use object::{Endianness, elf}; + use object::elf; + use object::read::elf::SectionHeader as _; let endian = match Elf::parse(data) { Ok(h) => match h.endian() { @@ -641,14 +716,13 @@ where Ok(s) => s, Err(_) => return None, }; - - let mut modified: Option> = None; + let e_shoff = u64::from(header.e_shoff(endian)) as usize; + let e_shnum = sections.len(); for section in sections.iter() { if section.sh_type(endian) != elf::SHT_SYMTAB { continue; } - let strtab_index = section.sh_link(endian) as usize; let strtab_section = match sections.section(object::SectionIndex(strtab_index)) { Ok(s) => s, @@ -658,68 +732,204 @@ where Ok(d) => d, Err(_) => continue, }; - - if sym_entry_size == 0 { - continue; - } - let sym_offset = u64::from(section.sh_offset(endian)) as usize; let sym_size = u64::from(section.sh_size(endian)) as usize; - let sym_count = sym_size / sym_entry_size; + let sym_count = sym_size / layout.sym_entry_size; + return Some(ElfSymtab { + endian, + sym_offset, + sym_count, + strtab_data, + layout, + e_shoff, + e_shnum, + strtab_section_index: strtab_index, + }); + } + None +} - let buf = modified.get_or_insert_with(|| data.to_vec()); +/// Detect ELF class and parse symtab. +fn elf_symtab_info(data: &[u8]) -> Option> { + use object::elf; + if data.len() < 16 || &data[0..4] != elf::ELFMAG { + return None; + } + match data[4] { + elf::ELFCLASS64 => { + elf_parse_symtab::>(data, &ElfSymLayout::ELF64) + } + elf::ELFCLASS32 => { + elf_parse_symtab::>(data, &ElfSymLayout::ELF32) + } + _ => None, + } +} - for i in 1..sym_count { - let off = sym_offset + i * sym_entry_size; - if off + sym_entry_size > buf.len() { - break; +/// Collect defined GLOBAL/WEAK symbol names from an ELF object that are NOT in +/// `keep_symbols`. These are the names that should be renamed. +fn elf_collect_rename_set( + data: &[u8], + keep_symbols: &FxHashSet, + out_set: &mut FxHashSet, +) { + use object::elf; + let Some(tab) = elf_symtab_info(data) else { return }; + for i in 1..tab.sym_count { + let off = tab.sym_off(i); + if off + tab.layout.sym_entry_size > data.len() { + break; + } + let binding = tab.binding(data, i); + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + if !tab.is_defined(data, i) { + continue; + } + if let Some(name) = tab.read_name(data, i) { + if !keep_symbols.contains(&name) { + out_set.insert(name); } + } + } +} - let st_info = buf[off + st_info_offset]; - let binding = st_info >> 4; +/// For ELF object files, rename GLOBAL/WEAK symbols whose names are in +/// `rename_set` by appending `suffix`, and set their visibility to `STV_HIDDEN`. +/// This handles both definitions AND undefined references, so cross-object-file +/// references remain consistent when the global rename_set was built from all +/// .o files in the archive. +/// +/// Uses the "move strtab to end" approach: builds a new strtab with renamed +/// names appended, places it at the end of the file, and patches the strtab +/// section header + ELF header. No other section offsets change. +/// +/// Returns `Some(modified_data)` if any changes were made, `None` if the data +/// is not an ELF object or no symbols matched the rename_set. +fn elf_apply_rename(data: &[u8], rename_set: &FxHashSet, suffix: &str) -> Option> { + use object::elf; + + let tab = elf_symtab_info(data)?; + if tab.sym_count <= 1 { + return None; + } - // Only process GLOBAL and WEAK symbols - if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { - continue; + // collect matching symbol names from this file + let mut matched_names: FxHashSet = FxHashSet::default(); + for i in 1..tab.sym_count { + let off = tab.sym_off(i); + if off + tab.layout.sym_entry_size > data.len() { + break; + } + let binding = tab.binding(data, i); + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + if let Some(name) = tab.read_name(data, i) { + if rename_set.contains(&name) { + matched_names.insert(name); } + } + } + if matched_names.is_empty() { + return None; + } - let st_shndx_offset = st_other_offset + 1; - let st_shndx_bytes: [u8; 2] = - buf[off + st_shndx_offset..off + st_shndx_offset + 2].try_into().unwrap_or([0, 0]); - let st_shndx = match endian { - Endianness::Little => u16::from_le_bytes(st_shndx_bytes), - Endianness::Big => u16::from_be_bytes(st_shndx_bytes), - }; - if st_shndx == elf::SHN_UNDEF as u16 { - continue; - } + // Build new strtab + let mut new_strtab: Vec = tab.strtab_data.to_vec(); + let mut rename_map: FxHashMap = FxHashMap::default(); - let st_name_bytes: [u8; 4] = buf[off..off + 4].try_into().unwrap_or([0; 4]); - let st_name_off = match endian { - Endianness::Little => u32::from_le_bytes(st_name_bytes), - Endianness::Big => u32::from_be_bytes(st_name_bytes), - } as usize; + #[allow(rustc::potential_query_instability)] + let mut sorted_names: Vec = matched_names.into_iter().collect(); + sorted_names.sort(); - if st_name_off >= strtab_data.len() { - continue; - } - let name_end = strtab_data[st_name_off..] - .iter() - .position(|&b| b == 0) - .unwrap_or(strtab_data.len() - st_name_off); - let name = match std::str::from_utf8(&strtab_data[st_name_off..st_name_off + name_end]) - { - Ok(s) => s, - Err(_) => continue, - }; + for name in &sorted_names { + let new_offset = new_strtab.len() as u32; + new_strtab.extend_from_slice(name.as_bytes()); + new_strtab.extend_from_slice(suffix.as_bytes()); + new_strtab.push(0); + rename_map.insert(name.clone(), new_offset); + } - if keep_symbols.contains(name) { - continue; - } + let new_strtab_size = new_strtab.len(); + + // [original data] [new strtab] [padding] [section headers] + let is_64 = tab.layout.is_64; + let e_shentsize = if is_64 { 64usize } else { 40 }; + let e_shoff = tab.e_shoff; + let e_shnum = tab.e_shnum; + let section_headers_size = e_shentsize * e_shnum; + let strtab_si = tab.strtab_section_index; + + let new_strtab_file_off = data.len(); + let new_e_shoff_raw = new_strtab_file_off + new_strtab_size; + let new_e_shoff = (new_e_shoff_raw + 3) & !3; + let padding_after_strtab = new_e_shoff - new_e_shoff_raw; + + let result_size = new_e_shoff + section_headers_size; + let mut result = Vec::with_capacity(result_size); + + result.extend_from_slice(data); + result.extend_from_slice(&new_strtab); + result.resize(result.len() + padding_after_strtab, 0); + if e_shoff + section_headers_size <= data.len() { + result.extend_from_slice(&data[e_shoff..e_shoff + section_headers_size]); + } else { + return None; + } + + // Patch strtab section header + let strtab_shdr_offset = new_e_shoff + strtab_si * e_shentsize; + if is_64 { + write_u64_at(&mut result, strtab_shdr_offset + 24, new_strtab_file_off as u64, tab.endian); + write_u64_at(&mut result, strtab_shdr_offset + 32, new_strtab_size as u64, tab.endian); + } else { + write_u32_at(&mut result, strtab_shdr_offset + 16, new_strtab_file_off as u32, tab.endian); + write_u32_at(&mut result, strtab_shdr_offset + 20, new_strtab_size as u32, tab.endian); + } + + // Patch ELF header e_shoff + if is_64 { + write_u64_at(&mut result, 40, new_e_shoff as u64, tab.endian); + } else { + write_u32_at(&mut result, 32, new_e_shoff as u32, tab.endian); + } - buf[off + st_other_offset] = elf::STV_HIDDEN; + // patch symtab entries (both defined AND undefined references) + let sym_offset = tab.sym_offset; + for i in 1..tab.sym_count { + let off = sym_offset + i * tab.layout.sym_entry_size; + if off + tab.layout.sym_entry_size > result.len() { + break; + } + let binding = tab.binding(&result, i); + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + if let Some(name) = tab.read_name(&result, i) { + if let Some(&new_st_name) = rename_map.get(&name) { + write_u32_at(&mut result, off, new_st_name, tab.endian); + result[off + tab.layout.st_other_offset] = elf::STV_HIDDEN; + } } } - modified + Some(result) +} + +fn write_u32_at(buf: &mut [u8], offset: usize, value: u32, endian: Endianness) { + let bytes = match endian { + Endianness::Little => value.to_le_bytes(), + Endianness::Big => value.to_be_bytes(), + }; + buf[offset..offset + 4].copy_from_slice(&bytes); +} + +fn write_u64_at(buf: &mut [u8], offset: usize, value: u64, endian: Endianness) { + let bytes = match endian { + Endianness::Little => value.to_le_bytes(), + Endianness::Big => value.to_be_bytes(), + }; + buf[offset..offset + 8].copy_from_slice(&bytes); } diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index 65bbd12620759..f461a0e2c0ba8 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -525,15 +525,23 @@ fn link_staticlib( sess.dcx().emit_fatal(e); } - if sess.opts.unstable_opts.staticlib_hide_internal_symbols { + if sess.opts.unstable_opts.staticlib_rename_internal_symbols { if !matches!(&*sess.target.archive_format, "gnu" | "bsd") { - sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported { + sess.dcx().emit_warn(errors::StaticlibRenameInternalSymbolsUnsupported { archive_format: sess.target.archive_format.to_string(), }); } else if let Some(symbols) = crate_info.exported_symbols.get(&CrateType::StaticLib) { use rustc_data_structures::fx::FxHashSet; let keep: FxHashSet = symbols.iter().map(|(s, _)| s.clone()).collect(); - ab.set_keep_symbols(keep); + // Generate a unique suffix from the crate name and a short hash + // extracted from the metadata symbol (format: rust_metadata_{name}_{hash:08x}). + let short_hash = crate_info + .metadata_symbol + .rsplit_once('_') + .map(|(_, hash)| hash.to_string()) + .unwrap_or_else(|| format!("{:08x}", crate_info.local_crate_name.as_u32())); + let suffix = format!("_rs{}", short_hash); + ab.set_rename_symbols(keep, suffix); } } diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs index 84a53c1ed28ae..89a08e17b8c01 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -677,9 +677,9 @@ pub(crate) struct BpfStaticlibNotSupported; #[derive(Diagnostic)] #[diag( - "-Zstaticlib-hide-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`" + "-Zstaticlib-rename-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`" )] -pub(crate) struct StaticlibHideInternalSymbolsUnsupported { +pub(crate) struct StaticlibRenameInternalSymbolsUnsupported { pub archive_format: String, } diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index bb5e63368ea9b..38f7576e4079f 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -869,7 +869,7 @@ fn test_unstable_options_tracking_hash() { tracked!(split_lto_unit, Some(true)); tracked!(src_hash_algorithm, Some(SourceFileHashAlgorithm::Sha1)); tracked!(stack_protector, StackProtector::All); - tracked!(staticlib_hide_internal_symbols, true); + tracked!(staticlib_rename_internal_symbols, true); tracked!(teach, true); tracked!(thinlto, Some(true)); tracked!(tiny_const_eval_limit, true); diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 48fc67f1e632f..f9aaefd60b767 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -2474,10 +2474,11 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options); - if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::StaticLib) + if unstable_opts.staticlib_rename_internal_symbols + && !crate_types.contains(&CrateType::StaticLib) { early_dcx.early_fatal( - "-Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib`", + "-Zstaticlib-rename-internal-symbols can only be used with `--crate-type staticlib`", ); } diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index f428cd86912d9..2b9944da511b0 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2764,8 +2764,8 @@ written to standard error output)"), "control stack smash protection strategy (`rustc --print stack-protector-strategies` for details)"), staticlib_allow_rdylib_deps: bool = (false, parse_bool, [TRACKED], "allow staticlibs to have rust dylib dependencies"), - staticlib_hide_internal_symbols: bool = (false, parse_bool, [TRACKED], - "hide Rust internal symbols when building staticlibs"), + staticlib_rename_internal_symbols: bool = (false, parse_bool, [TRACKED], + "rename Rust internal symbols when building staticlibs to avoid conflicts"), staticlib_prefer_dynamic: bool = (false, parse_bool, [TRACKED], "prefer dynamic linking to static linking for staticlibs (default: no)"), strict_init_checks: bool = (false, parse_bool, [TRACKED], diff --git a/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md deleted file mode 100644 index 1896e23f42147..0000000000000 --- a/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md +++ /dev/null @@ -1,10 +0,0 @@ -# `staticlib-hide-internal-symbols` - -When building a `staticlib`, this option hides all Rust-internal symbols -by setting their ELF visibility to `STV_HIDDEN`. - -This option can only be used with `--crate-type staticlib`. Using it with -other crate types will result in a compilation error. - -Currently only ELF targets are supported (Linux, BSD, etc.). On non-ELF -targets (macOS, Windows), a warning is emitted and the flag has no effect. diff --git a/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md new file mode 100644 index 0000000000000..720a0bdf61e2c --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md @@ -0,0 +1,16 @@ +# `staticlib-rename-internal-symbols` + +When building a `staticlib`, this option renames all non-exported Rust-internal +symbols by appending a `_rs{hash}` suffix and setting their ELF visibility to +`STV_HIDDEN`. This prevents symbol collisions when multiple Rust static +libraries are linked into the same final binary. + +Only symbols explicitly exported via `#[no_mangle]` or `#[export_name]` are left +unchanged. All other `GLOBAL`/`WEAK` symbols (including `pub(crate)` and `pub` +items without `#[no_mangle]`) are renamed. + +This option can only be used with `--crate-type staticlib`. Using it with +other crate types will result in a compilation error. + +Currently only ELF targets are supported (Linux, BSD, etc.). On non-ELF +targets (macOS, Windows), a warning is emitted and the flag has no effect. diff --git a/tests/run-make/staticlib-hide-internal-symbols/lib.rs b/tests/run-make/staticlib-rename-internal-symbols-visibility/lib.rs similarity index 100% rename from tests/run-make/staticlib-hide-internal-symbols/lib.rs rename to tests/run-make/staticlib-rename-internal-symbols-visibility/lib.rs diff --git a/tests/run-make/staticlib-hide-internal-symbols/main.c b/tests/run-make/staticlib-rename-internal-symbols-visibility/main.c similarity index 100% rename from tests/run-make/staticlib-hide-internal-symbols/main.c rename to tests/run-make/staticlib-rename-internal-symbols-visibility/main.c diff --git a/tests/run-make/staticlib-hide-internal-symbols/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols-visibility/rmake.rs similarity index 97% rename from tests/run-make/staticlib-hide-internal-symbols/rmake.rs rename to tests/run-make/staticlib-rename-internal-symbols-visibility/rmake.rs index 570d483642dd1..0e97bc8b00128 100644 --- a/tests/run-make/staticlib-hide-internal-symbols/rmake.rs +++ b/tests/run-make/staticlib-rename-internal-symbols-visibility/rmake.rs @@ -19,7 +19,7 @@ fn main() { rustc() .input("lib.rs") .crate_type("staticlib") - .arg("-Zstaticlib-hide-internal-symbols") + .arg("-Zstaticlib-rename-internal-symbols") .opt() .run(); @@ -83,7 +83,7 @@ fn check_symbols(archive_data: &[u8], with_flag: bool) { assert_eq!( vis, expected, - "with -Z: `{name}` should be {}, got {}", + "with -Z rename: `{name}` should be {}, got {}", visibility_name(expected), visibility_name(vis) ); diff --git a/tests/run-make/staticlib-rename-internal-symbols/dual_main.c b/tests/run-make/staticlib-rename-internal-symbols/dual_main.c new file mode 100644 index 0000000000000..21f4d5cae9b55 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/dual_main.c @@ -0,0 +1,14 @@ +extern int liba_process(int v); +extern int liba_answer(); +extern int libb_multiply(int a, int b); +extern int libb_greet(); + +int main() { + if (liba_answer() != 42) return 1; + if (liba_process(10) != 31) return 1; + + if (libb_multiply(6, 7) != 42) return 1; + if (libb_greet() != 99) return 1; + + return 0; +} diff --git a/tests/run-make/staticlib-rename-internal-symbols/lib.rs b/tests/run-make/staticlib-rename-internal-symbols/lib.rs new file mode 100644 index 0000000000000..4bbf21bf1918a --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/lib.rs @@ -0,0 +1,40 @@ +#![crate_type = "staticlib"] + +use std::collections::HashMap; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +#[no_mangle] +pub extern "C" fn my_add(a: i32, b: i32) -> i32 { + a + b +} + +#[no_mangle] +pub extern "C" fn my_hash_lookup(key: u64) -> u64 { + let mut map = HashMap::new(); + for i in 0..100u64 { + map.insert(i, i.wrapping_mul(2654435761)); + } + *map.get(&key).unwrap_or(&0) +} + +fn internal_helper() -> i32 { + 42 +} + +#[no_mangle] +pub extern "C" fn call_internal() -> i32 { + internal_helper() +} + +#[no_mangle] +pub extern "C" fn my_safe_div(a: i32, b: i32) -> i32 { + match catch_unwind(AssertUnwindSafe(|| { + if b == 0 { + panic!("division by zero!"); + } + a / b + })) { + Ok(result) => result, + Err(_) => -1, + } +} diff --git a/tests/run-make/staticlib-rename-internal-symbols/liba.rs b/tests/run-make/staticlib-rename-internal-symbols/liba.rs new file mode 100644 index 0000000000000..ca23944df44ea --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/liba.rs @@ -0,0 +1,17 @@ +#![crate_type = "staticlib"] + +mod internal { + pub fn compute(v: i32) -> i32 { + v * 3 + 1 + } +} + +#[no_mangle] +pub extern "C" fn liba_process(v: i32) -> i32 { + internal::compute(v) +} + +#[no_mangle] +pub extern "C" fn liba_answer() -> i32 { + 42 +} diff --git a/tests/run-make/staticlib-rename-internal-symbols/libb.rs b/tests/run-make/staticlib-rename-internal-symbols/libb.rs new file mode 100644 index 0000000000000..1eca8f3d254ca --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/libb.rs @@ -0,0 +1,15 @@ +#![crate_type = "staticlib"] + +fn internal_multiply(a: i32, b: i32) -> i32 { + a * b +} + +#[no_mangle] +pub extern "C" fn libb_multiply(a: i32, b: i32) -> i32 { + internal_multiply(a, b) +} + +#[no_mangle] +pub extern "C" fn libb_greet() -> i32 { + 99 +} diff --git a/tests/run-make/staticlib-rename-internal-symbols/main.c b/tests/run-make/staticlib-rename-internal-symbols/main.c new file mode 100644 index 0000000000000..580c805fed158 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/main.c @@ -0,0 +1,18 @@ +extern int my_add(int a, int b); +extern unsigned long my_hash_lookup(unsigned long key); +extern int call_internal(void); +extern int my_safe_div(int a, int b); + +int main() { + if (my_add(10, 20) != 30) + return 1; + if (my_hash_lookup(5) != 5UL * 2654435761UL) + return 1; + if (call_internal() != 42) + return 1; + if (my_safe_div(100, 5) != 20) + return 1; + if (my_safe_div(100, 0) != -1) + return 1; + return 0; +} diff --git a/tests/run-make/staticlib-rename-internal-symbols/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols/rmake.rs new file mode 100644 index 0000000000000..a1e278b285164 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/rmake.rs @@ -0,0 +1,172 @@ +//@ only-elf +//@ ignore-cross-compile + +use std::collections::HashSet; + +use run_make_support::object::Endianness; +use run_make_support::object::read::archive::ArchiveFile; +use run_make_support::object::read::elf::{FileHeader as _, SectionHeader as _, Sym as _}; +use run_make_support::{cc, extra_c_flags, object, rfs, run, rustc, static_lib_name}; + +type FileHeader64 = run_make_support::object::elf::FileHeader64; +type SymbolTable<'data> = run_make_support::object::read::elf::SymbolTable<'data, FileHeader64>; + +const EXPORTED: &[&str] = &["my_add", "my_hash_lookup", "call_internal", "my_safe_div"]; + +fn main() { + test_basic_functionality(); + test_rs_suffix_present(); + test_dual_staticlib_linking(); +} + +fn test_basic_functionality() { + let lib_name = static_lib_name("lib"); + + rustc() + .input("lib.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + cc().input("main.c").input(&lib_name).out_exe("main").args(extra_c_flags()).run(); + run("main"); + + rfs::remove_file(&lib_name); +} + +fn test_rs_suffix_present() { + let lib_name = static_lib_name("lib"); + + rustc() + .input("lib.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + let data = rfs::read(&lib_name); + let archive = ArchiveFile::parse(&*data).unwrap(); + let mut found_exported = HashSet::new(); + let mut found_rs_suffix = false; + + for member in archive.members() { + let member = member.unwrap(); + let member_name = std::str::from_utf8(member.name()).unwrap(); + if !member_name.ends_with(".rcgu.o") { + continue; + } + let member_data = member.data(&*data).unwrap(); + + let Ok(header) = FileHeader64::parse(member_data) else { continue }; + let Ok(endian) = header.endian() else { continue }; + let Ok(sections) = header.sections(endian, member_data) else { continue }; + + for (si, section) in sections.enumerate() { + if section.sh_type(endian) != object::elf::SHT_SYMTAB { + continue; + } + let Ok(symbols) = SymbolTable::parse(endian, member_data, §ions, si, section) + else { + continue; + }; + let strtab = symbols.strings(); + + for symbol in symbols.symbols() { + let vis = symbol.st_visibility(); + let bind = symbol.st_bind(); + let shndx = symbol.st_shndx(endian); + if shndx == object::elf::SHN_UNDEF as u16 { + continue; + } + if bind != object::elf::STB_GLOBAL && bind != object::elf::STB_WEAK { + continue; + } + + let Some(name) = read_symbol_name(endian, symbol, &strtab) else { continue }; + + if EXPORTED.contains(&name) { + assert!( + !name.contains("_rs"), + "exported symbol `{name}` should not contain _rs suffix" + ); + assert_eq!( + vis, + object::elf::STV_DEFAULT, + "exported symbol `{name}` should be STV_DEFAULT, got {}", + visibility_name(vis) + ); + found_exported.insert(name.to_string()); + } else { + assert!( + name.contains("_rs"), + "internal symbol `{name}` should contain _rs suffix after rename" + ); + assert_ne!( + vis, + object::elf::STV_DEFAULT, + "renamed internal symbol `{name}` should NOT be STV_DEFAULT" + ); + found_rs_suffix = true; + } + } + } + } + + assert!(found_rs_suffix, "expected to find at least one renamed symbol with _rs suffix"); + for expected in EXPORTED { + assert!( + found_exported.contains(*expected), + "expected to find exported symbol `{expected}` in archive" + ); + } + + rfs::remove_file(&lib_name); +} + +fn test_dual_staticlib_linking() { + let liba_name = static_lib_name("liba"); + let libb_name = static_lib_name("libb"); + + rustc() + .input("liba.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + rustc() + .input("libb.rs") + .crate_type("staticlib") + .arg("-Zstaticlib-rename-internal-symbols") + .opt() + .run(); + + cc().input("dual_main.c") + .input(&liba_name) + .input(&libb_name) + .out_exe("dual_main") + .args(extra_c_flags()) + .run(); + run("dual_main"); +} + +fn read_symbol_name<'data>( + endian: Endianness, + symbol: &run_make_support::object::elf::Sym64, + strtab: &object::StringTable<'data>, +) -> Option<&'data str> { + let bytes = strtab.get(symbol.st_name(endian)).ok()?; + let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + std::str::from_utf8(&bytes[..end]).ok() +} + +fn visibility_name(v: u8) -> &'static str { + match v { + v if v == object::elf::STV_DEFAULT => "STV_DEFAULT", + v if v == object::elf::STV_HIDDEN => "STV_HIDDEN", + v if v == object::elf::STV_INTERNAL => "STV_INTERNAL", + v if v == object::elf::STV_PROTECTED => "STV_PROTECTED", + _ => "UNKNOWN", + } +} diff --git a/tests/ui/README.md b/tests/ui/README.md index e0775a3a04cb8..3763ea47070a5 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1308,9 +1308,9 @@ See [Tracking Issue for stabilizing stack smashing protection (i.e., `-Z stack-p Tests on static items. -## `tests/ui/staticlib-hide-internal-symbols/`: `-Zstaticlib-hide-internal-symbols` command line flag +## `tests/ui/staticlib-rename-internal-symbols/`: `-Zstaticlib-rename-internal-symbols` command line flag -Tests for the `-Zstaticlib-hide-internal-symbols` flag, which hides non-exported symbols in ELF static libraries. +Tests for the `-Zstaticlib-rename-internal-symbols` flag, which renames non-exported symbols in ELF static libraries. ## `tests/ui/statics/` diff --git a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr deleted file mode 100644 index 999517b2d88d4..0000000000000 --- a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr +++ /dev/null @@ -1,2 +0,0 @@ -error: -Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib` - diff --git a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs b/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.rs similarity index 59% rename from tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs rename to tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.rs index 06663de81072d..128ac43a744f8 100644 --- a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs +++ b/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.rs @@ -1,4 +1,4 @@ -//@ compile-flags: -Zstaticlib-hide-internal-symbols --crate-type bin +//@ compile-flags: -Zstaticlib-rename-internal-symbols --crate-type bin #![feature(no_core)] #![no_core] diff --git a/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.stderr b/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.stderr new file mode 100644 index 0000000000000..c3482a5a95b5e --- /dev/null +++ b/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.stderr @@ -0,0 +1,2 @@ +error: -Zstaticlib-rename-internal-symbols can only be used with `--crate-type staticlib` + From 6bf8dccdcd5f628fd2c27284479120020392dc6e Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Fri, 17 Apr 2026 14:41:42 +0800 Subject: [PATCH 5/5] add `-Zstaticlib-hide-internal-symbols` --- .../rustc_codegen_ssa/src/back/archive.rs | 125 ++++++++++++++---- compiler/rustc_codegen_ssa/src/back/link.rs | 12 ++ compiler/rustc_codegen_ssa/src/errors.rs | 8 ++ compiler/rustc_interface/src/tests.rs | 1 + compiler/rustc_session/src/config.rs | 7 + compiler/rustc_session/src/options.rs | 2 + .../staticlib-hide-internal-symbols.md | 21 +++ .../staticlib-rename-internal-symbols.md | 11 +- .../lib.rs | 0 .../main.c | 0 .../rmake.rs | 4 +- .../rmake.rs | 5 - tests/ui/README.md | 4 + .../wrong-crate-type.rs | 7 + .../wrong-crate-type.stderr | 2 + 15 files changed, 171 insertions(+), 38 deletions(-) create mode 100644 src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md rename tests/run-make/{staticlib-rename-internal-symbols-visibility => staticlib-hide-internal-symbols}/lib.rs (100%) rename tests/run-make/{staticlib-rename-internal-symbols-visibility => staticlib-hide-internal-symbols}/main.c (100%) rename tests/run-make/{staticlib-rename-internal-symbols-visibility => staticlib-hide-internal-symbols}/rmake.rs (97%) create mode 100644 tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs create mode 100644 tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr diff --git a/compiler/rustc_codegen_ssa/src/back/archive.rs b/compiler/rustc_codegen_ssa/src/back/archive.rs index 94c5d6e51c43b..6e281fe95b297 100644 --- a/compiler/rustc_codegen_ssa/src/back/archive.rs +++ b/compiler/rustc_codegen_ssa/src/back/archive.rs @@ -320,6 +320,8 @@ pub trait ArchiveBuilder { fn build(self: Box, output: &Path) -> bool; + fn set_hide_symbols(&mut self, keep: FxHashSet); + fn set_rename_symbols(&mut self, keep: FxHashSet, suffix: String); } @@ -340,6 +342,7 @@ pub struct ArArchiveBuilder<'a> { // Don't use an `HashMap` here, as the order is important. `lib.rmeta` needs // to be at the end of an archive in some cases for linkers to not get confused. entries: Vec<(Vec, ArchiveEntry)>, + hide_symbols: Option>, rename_symbols: Option<(FxHashSet, String)>, } @@ -356,10 +359,15 @@ impl<'a> ArArchiveBuilder<'a> { object_reader, src_archives: vec![], entries: vec![], + hide_symbols: None, rename_symbols: None, } } + pub fn set_hide_symbols(&mut self, keep: FxHashSet) { + self.hide_symbols = Some(keep); + } + pub fn set_rename_symbols(&mut self, keep: FxHashSet, suffix: String) { self.rename_symbols = Some((keep, suffix)); } @@ -475,6 +483,10 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> { } } + fn set_hide_symbols(&mut self, keep: FxHashSet) { + self.hide_symbols = Some(keep); + } + fn set_rename_symbols(&mut self, keep: FxHashSet, suffix: String) { self.rename_symbols = Some((keep, suffix)); } @@ -495,12 +507,26 @@ impl<'a> ArArchiveBuilder<'a> { let mut entries = Vec::new(); - // When renaming symbols, we need a global two-pass approach: - // Pass 1: collect all non-exported defined symbol names across ALL .o files - // Pass 2: rename those symbols (both definitions and undefined references) in each .o file - // This ensures cross-object-file references remain consistent. - let global_rename_set: Option<(FxHashSet, &String)> = - self.rename_symbols.as_ref().map(|(keep, suffix)| { + // When hiding or renaming symbols, we need a global two-pass approach: + // 1: collect all non-exported defined symbol names across ALL .o files + // 2: apply hide/rename to each .o file + // For rename, this ensures cross-object-file references remain consistent. + let should_hide = self.hide_symbols.is_some(); + let should_rename = self.rename_symbols.is_some(); + + // Collect the internal symbol set in a dedicated scope so the borrow on + // self.hide_symbols / self.rename_symbols is released before the application loop. + let (global_internal_set, rename_suffix): (Option>, Option) = { + if !should_hide && !should_rename { + (None, None) + } else { + let keep: &FxHashSet = self + .rename_symbols + .as_ref() + .map(|(k, _)| k) + .or(self.hide_symbols.as_ref()) + .unwrap(); + let suffix = self.rename_symbols.as_ref().map(|(_, s)| s.clone()); let mut all_names: FxHashSet = FxHashSet::default(); for (_, entry) in &self.entries { let data: Option>> = match entry { @@ -524,8 +550,9 @@ impl<'a> ArArchiveBuilder<'a> { elf_collect_rename_set(data.as_ref().as_ref(), keep, &mut all_names); } } - (all_names, suffix) - }); + (Some(all_names), suffix) + } + }; for (entry_name, entry) in self.entries { let data: Box> = @@ -548,14 +575,26 @@ impl<'a> ArArchiveBuilder<'a> { }, }; - let data: Box> = if let Some((ref rename_set, suffix)) = - global_rename_set - { - if let Some(renamed) = elf_apply_rename(data.as_ref().as_ref(), rename_set, suffix) - { - Box::new(renamed) + let data: Box> = if let Some(ref internal_set) = global_internal_set { + if should_rename { + // rename (+ optionally hide) + if let Some(renamed) = elf_apply_rename( + data.as_ref().as_ref(), + internal_set, + rename_suffix.as_ref().unwrap(), + should_hide, + ) { + Box::new(renamed) + } else { + data + } } else { - data + // hide only (zero-overhead in-place modification) + if let Some(hidden) = elf_apply_hide(data.as_ref().as_ref(), internal_set) { + Box::new(hidden) + } else { + data + } } } else { data @@ -795,19 +834,51 @@ fn elf_collect_rename_set( } } +/// For ELF object files, hide GLOBAL/WEAK symbols whose names are in +/// `hide_set` by setting their visibility to `STV_HIDDEN`. +fn elf_apply_hide(data: &[u8], hide_set: &FxHashSet) -> Option> { + use object::elf; + + let tab = elf_symtab_info(data)?; + if tab.sym_count <= 1 { + return None; + } + + let mut result: Option> = None; + for i in 1..tab.sym_count { + let off = tab.sym_off(i); + if off + tab.layout.sym_entry_size > data.len() { + break; + } + let binding = tab.binding(data, i); + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + if !tab.is_defined(data, i) { + continue; + } + if let Some(name) = tab.read_name(data, i) { + if hide_set.contains(&name) { + let buf = result.get_or_insert_with(|| data.to_vec()); + buf[off + tab.layout.st_other_offset] = elf::STV_HIDDEN; + } + } + } + result +} + /// For ELF object files, rename GLOBAL/WEAK symbols whose names are in /// `rename_set` by appending `suffix`, and set their visibility to `STV_HIDDEN`. -/// This handles both definitions AND undefined references, so cross-object-file -/// references remain consistent when the global rename_set was built from all -/// .o files in the archive. /// -/// Uses the "move strtab to end" approach: builds a new strtab with renamed +/// move strtab to end: builds a new strtab with renamed /// names appended, places it at the end of the file, and patches the strtab /// section header + ELF header. No other section offsets change. -/// -/// Returns `Some(modified_data)` if any changes were made, `None` if the data -/// is not an ELF object or no symbols matched the rename_set. -fn elf_apply_rename(data: &[u8], rename_set: &FxHashSet, suffix: &str) -> Option> { +fn elf_apply_rename( + data: &[u8], + rename_set: &FxHashSet, + suffix: &str, + hide: bool, +) -> Option> { use object::elf; let tab = elf_symtab_info(data)?; @@ -836,7 +907,6 @@ fn elf_apply_rename(data: &[u8], rename_set: &FxHashSet, suffix: &str) - return None; } - // Build new strtab let mut new_strtab: Vec = tab.strtab_data.to_vec(); let mut rename_map: FxHashMap = FxHashMap::default(); @@ -879,7 +949,6 @@ fn elf_apply_rename(data: &[u8], rename_set: &FxHashSet, suffix: &str) - return None; } - // Patch strtab section header let strtab_shdr_offset = new_e_shoff + strtab_si * e_shentsize; if is_64 { write_u64_at(&mut result, strtab_shdr_offset + 24, new_strtab_file_off as u64, tab.endian); @@ -889,14 +958,12 @@ fn elf_apply_rename(data: &[u8], rename_set: &FxHashSet, suffix: &str) - write_u32_at(&mut result, strtab_shdr_offset + 20, new_strtab_size as u32, tab.endian); } - // Patch ELF header e_shoff if is_64 { write_u64_at(&mut result, 40, new_e_shoff as u64, tab.endian); } else { write_u32_at(&mut result, 32, new_e_shoff as u32, tab.endian); } - // patch symtab entries (both defined AND undefined references) let sym_offset = tab.sym_offset; for i in 1..tab.sym_count { let off = sym_offset + i * tab.layout.sym_entry_size; @@ -910,7 +977,9 @@ fn elf_apply_rename(data: &[u8], rename_set: &FxHashSet, suffix: &str) - if let Some(name) = tab.read_name(&result, i) { if let Some(&new_st_name) = rename_map.get(&name) { write_u32_at(&mut result, off, new_st_name, tab.endian); - result[off + tab.layout.st_other_offset] = elf::STV_HIDDEN; + if hide { + result[off + tab.layout.st_other_offset] = elf::STV_HIDDEN; + } } } } diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index f461a0e2c0ba8..ab770a202c5d2 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -525,6 +525,18 @@ fn link_staticlib( sess.dcx().emit_fatal(e); } + if sess.opts.unstable_opts.staticlib_hide_internal_symbols { + if !matches!(&*sess.target.archive_format, "gnu" | "bsd") { + sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported { + archive_format: sess.target.archive_format.to_string(), + }); + } else if let Some(symbols) = crate_info.exported_symbols.get(&CrateType::StaticLib) { + use rustc_data_structures::fx::FxHashSet; + let keep: FxHashSet = symbols.iter().map(|(s, _)| s.clone()).collect(); + ab.set_hide_symbols(keep); + } + } + if sess.opts.unstable_opts.staticlib_rename_internal_symbols { if !matches!(&*sess.target.archive_format, "gnu" | "bsd") { sess.dcx().emit_warn(errors::StaticlibRenameInternalSymbolsUnsupported { diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs index 89a08e17b8c01..83a78ad12899d 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -675,6 +675,14 @@ pub(crate) struct UnknownArchiveKind<'a> { #[diag("linking static libraries is not supported for BPF")] pub(crate) struct BpfStaticlibNotSupported; +#[derive(Diagnostic)] +#[diag( + "-Zstaticlib-hide-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`" +)] +pub(crate) struct StaticlibHideInternalSymbolsUnsupported { + pub archive_format: String, +} + #[derive(Diagnostic)] #[diag( "-Zstaticlib-rename-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`" diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index 38f7576e4079f..dc3c7d675ca65 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -869,6 +869,7 @@ fn test_unstable_options_tracking_hash() { tracked!(split_lto_unit, Some(true)); tracked!(src_hash_algorithm, Some(SourceFileHashAlgorithm::Sha1)); tracked!(stack_protector, StackProtector::All); + tracked!(staticlib_hide_internal_symbols, true); tracked!(staticlib_rename_internal_symbols, true); tracked!(teach, true); tracked!(thinlto, Some(true)); diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index f9aaefd60b767..360156238b5ad 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -2474,6 +2474,13 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options); + if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::StaticLib) + { + early_dcx.early_fatal( + "-Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib`", + ); + } + if unstable_opts.staticlib_rename_internal_symbols && !crate_types.contains(&CrateType::StaticLib) { diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 2b9944da511b0..f423745122806 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2764,6 +2764,8 @@ written to standard error output)"), "control stack smash protection strategy (`rustc --print stack-protector-strategies` for details)"), staticlib_allow_rdylib_deps: bool = (false, parse_bool, [TRACKED], "allow staticlibs to have rust dylib dependencies"), + staticlib_hide_internal_symbols: bool = (false, parse_bool, [TRACKED], + "hide non-exported symbols in ELF static libraries by setting STV_HIDDEN"), staticlib_rename_internal_symbols: bool = (false, parse_bool, [TRACKED], "rename Rust internal symbols when building staticlibs to avoid conflicts"), staticlib_prefer_dynamic: bool = (false, parse_bool, [TRACKED], diff --git a/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md new file mode 100644 index 0000000000000..819e168da8afa --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-hide-internal-symbols.md @@ -0,0 +1,21 @@ +# `staticlib-hide-internal-symbols` + +When building a `staticlib`, this option hides all non-exported Rust-internal +symbols by setting their ELF visibility to `STV_HIDDEN`. + +This is a lightweight, zero-overhead operation: only the visibility byte of each +internal symbol is modified in-place. No strtab manipulation or section header +copying is performed. + +Only symbols explicitly exported via `#[no_mangle]` or `#[export_name]` are left +unchanged. All other `GLOBAL`/`WEAK` symbols (including `pub(crate)` and `pub` +items without `#[no_mangle]`) are hidden. + +This option can only be used with `--crate-type staticlib`. Using it with +other crate types will result in a compilation error. + +Currently only ELF targets are supported (Linux, BSD, etc.). On non-ELF +targets (macOS, Windows), a warning is emitted and the flag has no effect. + +This option can be combined with `-Zstaticlib-rename-internal-symbols`. +When both are enabled, symbols are both renamed and hidden. diff --git a/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md index 720a0bdf61e2c..717b8a0dd114d 100644 --- a/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md @@ -1,9 +1,14 @@ # `staticlib-rename-internal-symbols` When building a `staticlib`, this option renames all non-exported Rust-internal -symbols by appending a `_rs{hash}` suffix and setting their ELF visibility to -`STV_HIDDEN`. This prevents symbol collisions when multiple Rust static -libraries are linked into the same final binary. +symbols by appending a `_rs{hash}` suffix. This prevents symbol collisions when +multiple Rust static libraries are linked into the same final binary. + +the Rust compiler already sets `STV_HIDDEN` visibility on non-exported +symbols by default in the generated `.o` files, so renamed internal symbols +retain their original `STV_HIDDEN` visibility even without +`-Zstaticlib-hide-internal-symbols`. Use `-Zstaticlib-hide-internal-symbols` +alone if you only need explicit visibility hiding without renaming (zero overhead). Only symbols explicitly exported via `#[no_mangle]` or `#[export_name]` are left unchanged. All other `GLOBAL`/`WEAK` symbols (including `pub(crate)` and `pub` diff --git a/tests/run-make/staticlib-rename-internal-symbols-visibility/lib.rs b/tests/run-make/staticlib-hide-internal-symbols/lib.rs similarity index 100% rename from tests/run-make/staticlib-rename-internal-symbols-visibility/lib.rs rename to tests/run-make/staticlib-hide-internal-symbols/lib.rs diff --git a/tests/run-make/staticlib-rename-internal-symbols-visibility/main.c b/tests/run-make/staticlib-hide-internal-symbols/main.c similarity index 100% rename from tests/run-make/staticlib-rename-internal-symbols-visibility/main.c rename to tests/run-make/staticlib-hide-internal-symbols/main.c diff --git a/tests/run-make/staticlib-rename-internal-symbols-visibility/rmake.rs b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs similarity index 97% rename from tests/run-make/staticlib-rename-internal-symbols-visibility/rmake.rs rename to tests/run-make/staticlib-hide-internal-symbols/rmake.rs index 0e97bc8b00128..a8732577321c8 100644 --- a/tests/run-make/staticlib-rename-internal-symbols-visibility/rmake.rs +++ b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs @@ -19,7 +19,7 @@ fn main() { rustc() .input("lib.rs") .crate_type("staticlib") - .arg("-Zstaticlib-rename-internal-symbols") + .arg("-Zstaticlib-hide-internal-symbols") .opt() .run(); @@ -83,7 +83,7 @@ fn check_symbols(archive_data: &[u8], with_flag: bool) { assert_eq!( vis, expected, - "with -Z rename: `{name}` should be {}, got {}", + "with -Z hide: `{name}` should be {}, got {}", visibility_name(expected), visibility_name(vis) ); diff --git a/tests/run-make/staticlib-rename-internal-symbols/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols/rmake.rs index a1e278b285164..86a01a3da5c16 100644 --- a/tests/run-make/staticlib-rename-internal-symbols/rmake.rs +++ b/tests/run-make/staticlib-rename-internal-symbols/rmake.rs @@ -102,11 +102,6 @@ fn test_rs_suffix_present() { name.contains("_rs"), "internal symbol `{name}` should contain _rs suffix after rename" ); - assert_ne!( - vis, - object::elf::STV_DEFAULT, - "renamed internal symbol `{name}` should NOT be STV_DEFAULT" - ); found_rs_suffix = true; } } diff --git a/tests/ui/README.md b/tests/ui/README.md index 3763ea47070a5..bd3333a08ac28 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1308,6 +1308,10 @@ See [Tracking Issue for stabilizing stack smashing protection (i.e., `-Z stack-p Tests on static items. +## `tests/ui/staticlib-hide-internal-symbols/`: `-Zstaticlib-hide-internal-symbols` command line flag + +Tests for the `-Zstaticlib-hide-internal-symbols` flag, which hides non-exported symbols in ELF static libraries. + ## `tests/ui/staticlib-rename-internal-symbols/`: `-Zstaticlib-rename-internal-symbols` command line flag Tests for the `-Zstaticlib-rename-internal-symbols` flag, which renames non-exported symbols in ELF static libraries. diff --git a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs new file mode 100644 index 0000000000000..06663de81072d --- /dev/null +++ b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.rs @@ -0,0 +1,7 @@ +//@ compile-flags: -Zstaticlib-hide-internal-symbols --crate-type bin + +#![feature(no_core)] +#![no_core] +#![no_main] + +//~? ERROR can only be used with `--crate-type staticlib` diff --git a/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr new file mode 100644 index 0000000000000..999517b2d88d4 --- /dev/null +++ b/tests/ui/staticlib-hide-internal-symbols/wrong-crate-type.stderr @@ -0,0 +1,2 @@ +error: -Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib` +