diff --git a/compiler/rustc_codegen_ssa/src/back/archive.rs b/compiler/rustc_codegen_ssa/src/back/archive.rs index 3f12e857391b2..6e281fe95b297 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::FxIndexSet; +use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet}; use rustc_data_structures::memmap::Mmap; use rustc_fs_util::TempDirBuilder; use rustc_metadata::EncodedMetadata; @@ -318,6 +319,10 @@ pub trait ArchiveBuilder { ) -> io::Result<()>; 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); } pub struct ArArchiveBuilderBuilder; @@ -337,6 +342,8 @@ 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)>, } #[derive(Debug)] @@ -347,7 +354,22 @@ 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![], + 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)); } } @@ -460,6 +482,14 @@ 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)); + } } impl<'a> ArArchiveBuilder<'a> { @@ -477,8 +507,55 @@ impl<'a> ArArchiveBuilder<'a> { let mut entries = Vec::new(); + // 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 { + 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); + } + } + (Some(all_names), suffix) + } + }; + 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 +575,31 @@ impl<'a> ArArchiveBuilder<'a> { }, }; + 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 { + // 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 + }; + entries.push(NewArchiveMember { buf: data, object_reader: self.object_reader, @@ -557,3 +659,346 @@ impl<'a> ArArchiveBuilder<'a> { fn io_error_context(context: &str, err: io::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, format!("{context}: {err}")) } + +/// 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, +} + +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 + } + + 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()) + } +} + +/// 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::elf; + use object::read::elf::SectionHeader as _; + + 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 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, + Err(_) => continue, + }; + let strtab_data = match strtab_section.data(endian, data) { + Ok(d) => d, + Err(_) => 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 / 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 +} + +/// 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, + } +} + +/// 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); + } + } + } +} + +/// 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`. +/// +/// 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. +fn elf_apply_rename( + data: &[u8], + rename_set: &FxHashSet, + suffix: &str, + hide: bool, +) -> Option> { + use object::elf; + + let tab = elf_symtab_info(data)?; + if tab.sym_count <= 1 { + return None; + } + + // 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 mut new_strtab: Vec = tab.strtab_data.to_vec(); + let mut rename_map: FxHashMap = FxHashMap::default(); + + #[allow(rustc::potential_query_instability)] + let mut sorted_names: Vec = matched_names.into_iter().collect(); + sorted_names.sort(); + + 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); + } + + 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; + } + + 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); + } + + 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); + } + + 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); + if hide { + result[off + tab.layout.st_other_offset] = elf::STV_HIDDEN; + } + } + } + } + + 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 cb22aac4e952d..ab770a202c5d2 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -525,6 +525,38 @@ 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 { + 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(); + // 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); + } + } + 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..83a78ad12899d 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -675,6 +675,22 @@ 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}`" +)] +pub(crate) struct StaticlibRenameInternalSymbolsUnsupported { + 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..dc3c7d675ca65 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -869,6 +869,8 @@ 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 3c0b3b4876659..360156238b5ad 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -2473,6 +2473,22 @@ 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`", + ); + } + + if unstable_opts.staticlib_rename_internal_symbols + && !crate_types.contains(&CrateType::StaticLib) + { + early_dcx.early_fatal( + "-Zstaticlib-rename-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..f423745122806 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2764,6 +2764,10 @@ 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], "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..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 new file mode 100644 index 0000000000000..717b8a0dd114d --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md @@ -0,0 +1,21 @@ +# `staticlib-rename-internal-symbols` + +When building a `staticlib`, this option renames all non-exported Rust-internal +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` +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-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..a8732577321c8 --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols/rmake.rs @@ -0,0 +1,132 @@ +//@ 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() { + 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 hide: `{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/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..86a01a3da5c16 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/rmake.rs @@ -0,0 +1,167 @@ +//@ 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" + ); + 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 9ef331698d2b6..bd3333a08ac28 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1308,6 +1308,14 @@ 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. + ## `tests/ui/statics/` **FIXME**: should probably be merged with `tests/ui/static/`. 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` + diff --git a/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.rs b/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.rs new file mode 100644 index 0000000000000..128ac43a744f8 --- /dev/null +++ b/tests/ui/staticlib-rename-internal-symbols/wrong-crate-type.rs @@ -0,0 +1,7 @@ +//@ compile-flags: -Zstaticlib-rename-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-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` +