From 55c16edc093f22a1979f7d1e1c354dfb28dcb108 Mon Sep 17 00:00:00 2001 From: cezarbbb Date: Tue, 9 Jun 2026 11:06:08 +0800 Subject: [PATCH] add -Zstaticlib-rename-internal-symbols --- .../rustc_codegen_ssa/src/back/archive.rs | 461 +++++++++++++++++- compiler/rustc_codegen_ssa/src/back/link.rs | 34 +- compiler/rustc_codegen_ssa/src/base.rs | 1 + compiler/rustc_codegen_ssa/src/errors.rs | 8 + compiler/rustc_codegen_ssa/src/lib.rs | 1 + compiler/rustc_interface/src/tests.rs | 1 + compiler/rustc_interface/src/util.rs | 2 +- compiler/rustc_session/src/config.rs | 8 + compiler/rustc_session/src/options.rs | 2 + .../staticlib-rename-internal-symbols.md | 19 + .../rmake.rs | 159 ++++++ .../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 | 20 + .../rmake.rs | 160 ++++++ ...ename-internal-symbols-wrong-crate-type.rs | 8 + ...e-internal-symbols-wrong-crate-type.stderr | 2 + 19 files changed, 950 insertions(+), 22 deletions(-) create mode 100644 src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md create mode 100644 tests/run-make/staticlib-rename-internal-symbols-macho/rmake.rs 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 create mode 100644 tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.rs create mode 100644 tests/ui/linking/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 66f9c7f8e15eb..c8925f9a9d48e 100644 --- a/compiler/rustc_codegen_ssa/src/back/archive.rs +++ b/compiler/rustc_codegen_ssa/src/back/archive.rs @@ -10,10 +10,10 @@ use ar_archive_writer::{ }; pub use ar_archive_writer::{DEFAULT_OBJECT_READER, ObjectReader}; use object::read::archive::{ArchiveFile, ArchiveKind as ObjectArchiveKind}; -use object::read::elf::Sym as _; +use object::read::elf::{SectionHeader as _, Sym as _}; use object::read::macho::{FatArch, Nlist}; use object::{Endianness, elf, macho}; -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; @@ -320,7 +320,13 @@ pub trait ArchiveBuilder { fn add_archive(&mut self, archive: &Path, kind: AddArchiveKind<'_>) -> io::Result<()>; - fn build(self: Box, output: &Path, exported_symbols: Option>) -> bool; + fn build( + self: Box, + output: &Path, + exported_symbols: Option>, + rename_suffix: Option, + hide: bool, + ) -> bool; } fn target_archive_format_to_object_kind(format: &str) -> Option { @@ -535,9 +541,15 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> { /// Combine the provided files, rlibs, and native libraries into a single /// `Archive`. - fn build(self: Box, output: &Path, exported_symbols: Option>) -> bool { + fn build( + self: Box, + output: &Path, + exported_symbols: Option>, + rename_suffix: Option, + hide: bool, + ) -> bool { let sess = self.sess; - match self.build_inner(output, exported_symbols) { + match self.build_inner(output, exported_symbols, rename_suffix, hide) { Ok(any_members) => any_members, Err(error) => { sess.dcx().emit_fatal(ArchiveBuildFailure { path: output.to_owned(), error }) @@ -551,6 +563,8 @@ impl<'a> ArArchiveBuilder<'a> { self, output: &Path, exported_symbols: Option>, + rename_suffix: Option, + hide: bool, ) -> io::Result { let archive_kind = match &*self.sess.target.archive_format { "gnu" => ArchiveKind::Gnu, @@ -563,6 +577,37 @@ impl<'a> ArArchiveBuilder<'a> { } }; + // Rename requires a two-pass approach: first collect all internal symbols + // across all .o files to ensure consistent renaming, then apply. + let global_rename_set = if let Some(suffix) = &rename_suffix + && let Some(exported) = &exported_symbols + { + let mut all_names = FxHashSet::default(); + for (_, entry) in &self.entries { + if entry.kind != ArchiveEntryKind::RustObj { + continue; + } + match &entry.source { + ArchiveEntrySource::Archive { archive_index, file_range } => { + let src_archive = &self.src_archives[*archive_index]; + let start = file_range.0 as usize; + let end = start + file_range.1 as usize; + if let Some(data) = src_archive.1.get(start..end) { + collect_internal_set(data, exported, &mut all_names); + } + } + ArchiveEntrySource::File(file) => { + if let Ok(data) = std::fs::read(file) { + collect_internal_set(&data, exported, &mut all_names); + } + } + } + } + Some((all_names, suffix.as_str())) + } else { + None + }; + let mut entries = Vec::new(); for (entry_name, entry) in self.entries { @@ -588,10 +633,18 @@ impl<'a> ArArchiveBuilder<'a> { )); }; - if entry.kind == ArchiveEntryKind::RustObj - && let Some(exported) = &exported_symbols - { - Box::new(apply_hide(data, exported)) + if entry.kind == ArchiveEntryKind::RustObj { + let renamed = global_rename_set + .as_ref() + .and_then(|(rename_set, suffix)| rename_impl(data, rename_set, suffix)); + if hide && let Some(exported) = &exported_symbols { + let src = renamed.as_deref().unwrap_or(data); + Box::new(apply_hide(src, exported)) + } else if let Some(renamed) = renamed { + Box::new(renamed) + } else { + Box::new(data) + } } else { Box::new(data) } @@ -602,10 +655,22 @@ impl<'a> ArArchiveBuilder<'a> { .map_err(|err| io_error_context("failed to open object file", err))?, ) .map_err(|err| io_error_context("failed to map object file", err))?; - if entry.kind == ArchiveEntryKind::RustObj - && let Some(exported) = &exported_symbols - { - Box::new(apply_hide(&mmap, exported)) + if entry.kind == ArchiveEntryKind::RustObj { + let renamed = + global_rename_set.as_ref().and_then(|(rename_set, suffix)| { + rename_impl(&mmap, rename_set, suffix) + }); + if hide && let Some(exported) = &exported_symbols { + let src = match &renamed { + Some(v) => v.as_slice(), + None => &*mmap, + }; + Box::new(apply_hide(src, exported)) + } else if let Some(renamed) = renamed { + Box::new(renamed) + } else { + Box::new(mmap) as Box> + } } else { Box::new(mmap) as Box> } @@ -821,3 +886,373 @@ fn apply_hide(data: &[u8], exported: &FxHashSet) -> Vec { let patches = hide_patches(data, exported).unwrap_or_default(); apply_patches(data, &patches) } + +// --------------------------------------------------------------------------- +// Rename: collect internal symbol names across all .o files +// --------------------------------------------------------------------------- + +struct RenameEntry { + name_field_offset: usize, + name: String, +} + +fn build_renamed_strtab( + old_strtab: &[u8], + renames: &[RenameEntry], + suffix: &str, +) -> (Vec, FxHashMap) { + let mut new_strtab = old_strtab.to_vec(); + let mut rename_map: FxHashMap = FxHashMap::default(); + + let mut sorted_names: Vec<&str> = renames.iter().map(|r| r.name.as_str()).collect(); + sorted_names.sort(); + sorted_names.dedup(); + + 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.to_string(), new_offset); + } + + (new_strtab, rename_map) +} + +fn collect_internal_set(data: &[u8], exported: &FxHashSet, out: &mut FxHashSet) { + let Ok(file) = object::File::parse(data) else { return }; + match file { + object::File::Elf64(_) => { + elf_collect_internal_impl::>(data, exported, out) + } + object::File::Elf32(_) => { + elf_collect_internal_impl::>(data, exported, out) + } + object::File::MachO64(_) => { + macho_collect_internal_impl::>(data, exported, out) + } + object::File::MachO32(_) => { + macho_collect_internal_impl::>(data, exported, out) + } + _ => {} + } +} + +fn elf_collect_internal_impl>( + data: &[u8], + exported: &FxHashSet, + out: &mut FxHashSet, +) where + u64: From, +{ + let Ok(header) = Elf::parse(data) else { return }; + let Ok(endian) = header.endian() else { return }; + let Ok(sections) = header.sections(endian, data) else { return }; + let Ok(symtab) = sections.symbols(endian, data, elf::SHT_SYMTAB) else { return }; + let strings = symtab.strings(); + + for sym in symtab.iter() { + let binding = sym.st_bind(); + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + if sym.is_undefined(endian) { + continue; + } + let Ok(name_bytes) = sym.name(endian, strings) else { continue }; + let Ok(name) = str::from_utf8(name_bytes) else { continue }; + if !exported.contains(name) { + out.insert(name.to_string()); + } + } +} + +fn macho_collect_internal_impl>( + data: &[u8], + exported: &FxHashSet, + out: &mut FxHashSet, +) { + let Ok(header) = Mach::parse(data, 0) else { return }; + let Ok(endian) = header.endian() else { return }; + let Ok(mut commands) = header.load_commands(endian, data, 0) else { return }; + + let symtab_cmd = loop { + let Ok(Some(cmd)) = commands.next() else { return }; + if let Ok(Some(st)) = cmd.symtab() { + break st; + } + }; + let Ok(symtab) = symtab_cmd.symbols::(endian, data) else { return }; + let strings = symtab.strings(); + + for nlist in symtab.iter() { + if nlist.is_stab() { + continue; + } + if nlist.is_undefined() { + continue; + } + if nlist.n_type() & macho::N_EXT == 0 { + continue; + } + let Ok(name_bytes) = nlist.name(endian, strings) else { continue }; + let Ok(name) = str::from_utf8(name_bytes) else { continue }; + let name = name.strip_prefix('_').unwrap_or(name); + if !exported.contains(name) { + out.insert(name.to_string()); + } + } +} + +// --------------------------------------------------------------------------- +// Rename: apply rename to a single object file +// --------------------------------------------------------------------------- + +fn rename_impl(data: &[u8], rename_set: &FxHashSet, suffix: &str) -> Option> { + let file = object::File::parse(data).ok()?; + match file { + object::File::Elf64(_) => { + elf_apply_rename_impl::>(data, rename_set, suffix) + } + object::File::Elf32(_) => { + elf_apply_rename_impl::>(data, rename_set, suffix) + } + object::File::MachO64(_) => { + macho_apply_rename_impl::>(data, rename_set, suffix) + } + object::File::MachO32(_) => { + macho_apply_rename_impl::>(data, rename_set, suffix) + } + _ => None, + } +} + +fn elf_apply_rename_impl>( + data: &[u8], + rename_set: &FxHashSet, + suffix: &str, +) -> Option> +where + u64: From, +{ + let header = Elf::parse(data).ok()?; + let endian = header.endian().ok()?; + let sections = header.sections(endian, data).ok()?; + let symtab = sections.symbols(endian, data, elf::SHT_SYMTAB).ok()?; + let strings = symtab.strings(); + + let data_ptr = data.as_ptr() as usize; + + let mut renames: Vec = Vec::new(); + + for sym in symtab.iter() { + let binding = sym.st_bind(); + if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK { + continue; + } + let Ok(name_bytes) = sym.name(endian, strings) else { continue }; + let Ok(name) = str::from_utf8(name_bytes) else { continue }; + if rename_set.contains(name) { + let sym_addr = sym as *const Elf::Sym as usize; + renames.push(RenameEntry { + name_field_offset: sym_addr - data_ptr, + name: name.to_string(), + }); + } + } + + if renames.is_empty() { + return None; + } + + // Find strtab section index via symtab's sh_link + let mut strtab_si: Option = None; + for section in sections.iter() { + if section.sh_type(endian) == elf::SHT_SYMTAB { + strtab_si = Some(section.sh_link(endian) as usize); + break; + } + } + let strtab_si = strtab_si?; + + let e_shoff = u64::from(header.e_shoff(endian)) as usize; + let e_shentsize = mem::size_of::(); + let e_shnum = sections.len(); + + let strtab_section = sections.section(object::SectionIndex(strtab_si)).ok()?; + let old_strtab_offset = u64::from(strtab_section.sh_offset(endian)) as usize; + let old_strtab_size = u64::from(strtab_section.sh_size(endian)) as usize; + let old_strtab = data.get(old_strtab_offset..old_strtab_offset + old_strtab_size)?; + + let (new_strtab, rename_map) = build_renamed_strtab(old_strtab, &renames, suffix); + + // Output layout: [original data][new strtab][padding][section headers] + let is_64 = mem::size_of::() == 8; + let new_strtab_file_off = data.len(); + let new_strtab_size = new_strtab.len(); + let new_e_shoff_raw = new_strtab_file_off + new_strtab_size; + let new_e_shoff = (new_e_shoff_raw + 7) & !7; // 8-byte align + let padding = new_e_shoff - new_e_shoff_raw; + let section_headers_size = e_shentsize * e_shnum; + + 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, 0); + let sh_data = data.get(e_shoff..e_shoff + section_headers_size)?; + result.extend_from_slice(sh_data); + + // Patch ELF header e_shoff + if is_64 { + write_u64_at( + &mut result, + mem::offset_of!(elf::FileHeader64, e_shoff), + new_e_shoff as u64, + endian, + ); + } else { + write_u32_at( + &mut result, + mem::offset_of!(elf::FileHeader32, e_shoff), + new_e_shoff as u32, + endian, + ); + } + + // Patch strtab section header: sh_offset + sh_size + let new_strtab_shdr_offset = new_e_shoff + strtab_si * e_shentsize; + + if is_64 { + let sh_offset_field = mem::offset_of!(elf::SectionHeader64, sh_offset); + let sh_size_field = mem::offset_of!(elf::SectionHeader64, sh_size); + write_u64_at( + &mut result, + new_strtab_shdr_offset + sh_offset_field, + new_strtab_file_off as u64, + endian, + ); + write_u64_at( + &mut result, + new_strtab_shdr_offset + sh_size_field, + new_strtab_size as u64, + endian, + ); + } else { + let sh_offset_field = mem::offset_of!(elf::SectionHeader32, sh_offset); + let sh_size_field = mem::offset_of!(elf::SectionHeader32, sh_size); + write_u32_at( + &mut result, + new_strtab_shdr_offset + sh_offset_field, + new_strtab_file_off as u32, + endian, + ); + write_u32_at( + &mut result, + new_strtab_shdr_offset + sh_size_field, + new_strtab_size as u32, + endian, + ); + } + + // Patch each renamed symbol's st_name + for entry in &renames { + let Some(&new_st_name) = rename_map.get(&entry.name) else { continue }; + write_u32_at(&mut result, entry.name_field_offset, new_st_name, endian); + } + + Some(result) +} + +fn macho_apply_rename_impl>( + data: &[u8], + rename_set: &FxHashSet, + suffix: &str, +) -> Option> { + let header = Mach::parse(data, 0).ok()?; + let endian = header.endian().ok()?; + let mut commands = header.load_commands(endian, data, 0).ok()?; + + let data_ptr = data.as_ptr() as usize; + + let (symtab_cmd, symtab_cmd_offset) = loop { + let Ok(Some(cmd)) = commands.next() else { return None }; + if let Ok(Some(st)) = cmd.symtab() { + break (st, cmd.raw_data().as_ptr() as usize - data_ptr); + } + }; + + let symtab: object::read::macho::SymbolTable<'_, Mach, &_> = + symtab_cmd.symbols(endian, data).ok()?; + let strings = symtab.strings(); + + let mut renames: Vec = Vec::new(); + + for nlist in symtab.iter() { + if nlist.is_stab() { + continue; + } + if nlist.n_type() & macho::N_EXT == 0 { + continue; + } + let Ok(name_bytes) = nlist.name(endian, strings) else { continue }; + let Ok(raw_name) = str::from_utf8(name_bytes) else { continue }; + let name = raw_name.strip_prefix('_').unwrap_or(raw_name); + if rename_set.contains(name) { + let nlist_addr = nlist as *const Mach::Nlist as usize; + renames.push(RenameEntry { + name_field_offset: nlist_addr - data_ptr, + name: raw_name.to_string(), + }); + } + } + + if renames.is_empty() { + return None; + } + + // Build new strtab + let old_stroff = symtab_cmd.stroff.get(endian) as usize; + let old_strsize = symtab_cmd.strsize.get(endian) as usize; + let old_strtab = data.get(old_stroff..old_stroff + old_strsize)?; + + let (new_strtab, rename_map) = build_renamed_strtab(old_strtab, &renames, suffix); + + // Output: [original data][new strtab] + let new_strtab_file_off = data.len(); + let new_strtab_size = new_strtab.len(); + + let mut result = Vec::with_capacity(data.len() + new_strtab_size); + result.extend_from_slice(data); + result.extend_from_slice(&new_strtab); + + // Patch LC_SYMTAB: stroff and strsize within the command + let stroff_off = mem::offset_of!(macho::SymtabCommand, stroff); + let strsize_off = mem::offset_of!(macho::SymtabCommand, strsize); + write_u32_at(&mut result, symtab_cmd_offset + stroff_off, new_strtab_file_off as u32, endian); + write_u32_at(&mut result, symtab_cmd_offset + strsize_off, new_strtab_size as u32, endian); + + // Patch each renamed nlist's n_strx + for entry in &renames { + let Some(&new_strx) = rename_map.get(&entry.name) else { continue }; + write_u32_at(&mut result, entry.name_field_offset, new_strx, endian); + } + + 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 c68220aea78f7..bf45d7af32b81 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -131,7 +131,7 @@ pub fn link_binary( RlibFlavor::Normal, &path, ) - .build(&out_filename, None); + .build(&out_filename, None, None, false); } CrateType::StaticLib => { link_staticlib( @@ -566,11 +566,21 @@ fn link_staticlib( sess.dcx().emit_fatal(e); } - let exported_symbols = if sess.opts.unstable_opts.staticlib_hide_internal_symbols { + let hide = sess.opts.unstable_opts.staticlib_hide_internal_symbols; + let rename = sess.opts.unstable_opts.staticlib_rename_internal_symbols; + + let exported_symbols = if hide || rename { if !matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO) { - sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported { - binary_format: sess.target.archive_format.to_string(), - }); + if hide { + sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported { + binary_format: sess.target.archive_format.to_string(), + }); + } + if rename { + sess.dcx().emit_warn(errors::StaticlibRenameInternalSymbolsUnsupported { + binary_format: sess.target.archive_format.to_string(), + }); + } None } else { crate_info @@ -581,7 +591,15 @@ fn link_staticlib( } else { None }; - ab.build(out_filename, exported_symbols); + + let rename_suffix = + if rename && matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO) { + Some(crate_info.rename_suffix.clone()) + } else { + None + }; + + ab.build(out_filename, exported_symbols, rename_suffix, hide); let crates = crate_info.used_crates.iter(); @@ -1280,7 +1298,7 @@ fn link_natively( if should_archive { let mut ab = archive_builder_builder.new_archive_builder(sess); ab.add_file(temp_filename, ArchiveEntryKind::Other); - ab.build(out_filename, None); + ab.build(out_filename, None, None, false); } } @@ -3280,7 +3298,7 @@ fn add_static_crate( sess.dcx() .emit_fatal(errors::RlibArchiveBuildFailure { path: cratepath.clone(), error }); } - if archive.build(&dst, None) { + if archive.build(&dst, None, None, false) { link_upstream(&dst); } }); diff --git a/compiler/rustc_codegen_ssa/src/base.rs b/compiler/rustc_codegen_ssa/src/base.rs index c14d4aec48c35..c5e446bf88fb5 100644 --- a/compiler/rustc_codegen_ssa/src/base.rs +++ b/compiler/rustc_codegen_ssa/src/base.rs @@ -962,6 +962,7 @@ impl CrateInfo { natvis_debugger_visualizers: Default::default(), lint_level_specs: CodegenLintLevelSpecs::from_tcx(tcx), metadata_symbol: exported_symbols::metadata_symbol_name(tcx), + rename_suffix: format!("_rs{:x}", tcx.stable_crate_id(LOCAL_CRATE)), each_linked_rlib_file_for_lto: Default::default(), exported_symbols_for_lto: Default::default(), }; diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs index 9326e477465f0..37352eaa7d55f 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -701,6 +701,14 @@ pub(crate) struct StaticlibHideInternalSymbolsUnsupported { pub binary_format: String, } +#[derive(Diagnostic)] +#[diag( + "-Zstaticlib-rename-internal-symbols only supports ELF and Mach-O targets, but the target uses `{$binary_format}`" +)] +pub(crate) struct StaticlibRenameInternalSymbolsUnsupported { + pub binary_format: String, +} + #[derive(Diagnostic)] #[diag("entry symbol `main` declared multiple times")] #[help( diff --git a/compiler/rustc_codegen_ssa/src/lib.rs b/compiler/rustc_codegen_ssa/src/lib.rs index 560cbe98f7497..c0ea59a879594 100644 --- a/compiler/rustc_codegen_ssa/src/lib.rs +++ b/compiler/rustc_codegen_ssa/src/lib.rs @@ -259,6 +259,7 @@ pub struct CrateInfo { pub natvis_debugger_visualizers: BTreeSet, pub lint_level_specs: CodegenLintLevelSpecs, pub metadata_symbol: String, + pub rename_suffix: String, pub each_linked_rlib_file_for_lto: Vec, pub exported_symbols_for_lto: Vec, } diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index 622077caddc40..7bbb651889fd8 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -867,6 +867,7 @@ fn test_unstable_options_tracking_hash() { 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_interface/src/util.rs b/compiler/rustc_interface/src/util.rs index 6e74ec64ad241..09a730e9e2620 100644 --- a/compiler/rustc_interface/src/util.rs +++ b/compiler/rustc_interface/src/util.rs @@ -470,7 +470,7 @@ impl ArchiveBuilderBuilder for DummyArchiveBuilderBuilder { output_path: &Path, ) { // Build an empty static library to avoid calling an external dlltool on mingw - ArArchiveBuilderBuilder.new_archive_builder(sess).build(output_path, None); + ArArchiveBuilderBuilder.new_archive_builder(sess).build(output_path, None, None, false); } } diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 1b3217ed0a030..a712e55bc4ce1 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 ); } + if unstable_opts.staticlib_rename_internal_symbols + && !crate_types.contains(&CrateType::StaticLib) + { + early_dcx.early_warn( + "-Zstaticlib-rename-internal-symbols has no effect without `--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 6ae0d9721bc00..5e9885c391781 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2690,6 +2690,8 @@ written to standard error output)"), "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-rename-internal-symbols.md b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md new file mode 100644 index 0000000000000..f25a45ae7efc9 --- /dev/null +++ b/src/doc/unstable-book/src/compiler-flags/staticlib-rename-internal-symbols.md @@ -0,0 +1,19 @@ +# `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. + +This option only renames symbols; it does not change their visibility. +Use `-Zstaticlib-hide-internal-symbols` in addition if you also want to hide +internal symbols. + +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 warning. + +Supported on ELF targets (Linux, BSD, etc.) and Apple targets (macOS, iOS, etc.). +On unsupported targets (Windows), a warning is emitted and the flag has no effect. diff --git a/tests/run-make/staticlib-rename-internal-symbols-macho/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols-macho/rmake.rs new file mode 100644 index 0000000000000..cf01600ad00ea --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/rmake.rs @@ -0,0 +1,159 @@ +//@ only-apple +//@ ignore-cross-compile + +use std::collections::HashSet; + +use run_make_support::object::Endianness; +use run_make_support::object::macho::{MachHeader64, N_EXT, N_PEXT, N_SECT, N_STAB, N_TYPE}; +use run_make_support::object::read::archive::ArchiveFile; +use run_make_support::object::read::macho::{MachHeader as _, Nlist as _}; +use run_make_support::path_helpers::source_root; +use run_make_support::{cc, extra_c_flags, object, rfs, run, rustc, static_lib_name}; + +type MachOFileHeader64 = MachHeader64; +type SymbolTable<'data> = + run_make_support::object::read::macho::SymbolTable<'data, MachOFileHeader64>; + +const EXPORTED: &[&str] = &["my_add", "my_hash_lookup", "call_internal", "my_safe_div"]; + +fn main() { + let sibling = source_root().join("tests/run-make/staticlib-rename-internal-symbols"); + rfs::copy(sibling.join("lib.rs"), "lib.rs"); + rfs::copy(sibling.join("main.c"), "main.c"); + rfs::copy(sibling.join("liba.rs"), "liba.rs"); + rfs::copy(sibling.join("libb.rs"), "libb.rs"); + rfs::copy(sibling.join("dual_main.c"), "dual_main.c"); + + test_basic_functionality(); + test_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_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); + check_rename_symbols(&data); + + 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 check_rename_symbols(archive_data: &[u8]) { + let archive = ArchiveFile::parse(archive_data).unwrap(); + let mut found_exported = HashSet::new(); + let mut found_suffix = false; + + for member in archive.members() { + let member = member.unwrap(); + let data = member.data(archive_data).unwrap(); + + let Ok(header) = MachOFileHeader64::parse(data, 0) else { continue }; + let Ok(endian) = header.endian() else { continue }; + + let Some(symtab) = find_symtab(header, endian, data) else { continue }; + let strtab = symtab.strings(); + + for nlist in symtab.iter() { + let n_type = nlist.n_type(); + if n_type & N_STAB != 0 { + continue; + } + if n_type & N_EXT == 0 { + continue; + } + if n_type & N_TYPE != N_SECT { + continue; + } + + let Ok(name_bytes) = nlist.name(endian, strtab) else { continue }; + let Ok(name) = std::str::from_utf8(name_bytes) else { continue }; + let name = name.strip_prefix('_').unwrap_or(name); + + if EXPORTED.contains(&name) { + assert!( + !name.contains("_rs"), + "exported symbol `{name}` should not contain _rs suffix" + ); + found_exported.insert(name.to_string()); + } else { + assert!( + name.contains("_rs"), + "internal symbol `{name}` should contain _rs suffix after rename" + ); + found_suffix = true; + } + } + } + + assert!(found_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" + ); + } +} + +fn find_symtab<'data>( + header: &MachOFileHeader64, + endian: Endianness, + data: &'data [u8], +) -> Option> { + let mut commands = header.load_commands(endian, data, 0).ok()?; + while let Ok(Some(command)) = commands.next() { + if let Ok(Some(symtab_cmd)) = command.symtab() { + return symtab_cmd.symbols::(endian, data).ok(); + } + } + None +} 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..ad8b26e6ce950 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/main.c @@ -0,0 +1,20 @@ +#include + +extern int my_add(int a, int b); +extern uint64_t my_hash_lookup(uint64_t 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) != (uint64_t)5 * 2654435761ULL) + 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..18440305a4e3f --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols/rmake.rs @@ -0,0 +1,160 @@ +//@ only-elf +//@ ignore-cross-compile + +use std::collections::HashSet; + +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::object::{Endianness, elf}; +use run_make_support::{cc, extra_c_flags, rfs, run, rustc, static_lib_name}; + +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); + check_rename_symbols(&data); + + 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 check_rename_symbols(archive_data: &[u8]) { + let archive = ArchiveFile::parse(archive_data).unwrap(); + let mut found_exported = HashSet::new(); + let mut found_rs_suffix = false; + + for member in archive.members() { + let member = member.unwrap(); + if !member.name().ends_with(b".rcgu.o") { + continue; + } + let data = member.data(archive_data).unwrap(); + + if let Ok(header) = elf::FileHeader64::::parse(data) { + check_elf_symbols(header, data, &mut found_exported, &mut found_rs_suffix); + } else if let Ok(header) = elf::FileHeader32::::parse(data) { + check_elf_symbols(header, data, &mut found_exported, &mut found_rs_suffix); + } + } + + 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" + ); + } +} + +fn check_elf_symbols>( + header: &Elf, + data: &[u8], + found_exported: &mut HashSet, + found_rs_suffix: &mut bool, +) { + let Ok(endian) = header.endian() else { return }; + let Ok(sections) = header.sections(endian, data) else { return }; + + for (si, section) in sections.enumerate() { + if section.sh_type(endian) != elf::SHT_SYMTAB { + continue; + } + let Ok(symbols) = run_make_support::object::read::elf::SymbolTable::parse( + endian, data, §ions, si, section, + ) else { + continue; + }; + let strtab = symbols.strings(); + + for symbol in symbols.symbols() { + let bind = symbol.st_bind(); + let shndx = symbol.st_shndx(endian); + + if shndx == elf::SHN_UNDEF { + continue; + } + if bind != elf::STB_GLOBAL && bind != elf::STB_WEAK { + continue; + } + + let Ok(name_bytes) = symbol.name(endian, strtab) else { continue }; + let Ok(name) = str::from_utf8(name_bytes) else { continue }; + + if EXPORTED.contains(&name) { + assert!( + !name.contains("_rs"), + "exported symbol `{name}` should not contain _rs suffix" + ); + assert_eq!( + symbol.st_visibility(), + elf::STV_DEFAULT, + "exported symbol `{name}` should be STV_DEFAULT" + ); + found_exported.insert(name.to_string()); + } else { + assert!( + name.contains("_rs"), + "internal symbol `{name}` should contain _rs suffix after rename" + ); + *found_rs_suffix = true; + } + } + } +} diff --git a/tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.rs b/tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.rs new file mode 100644 index 0000000000000..684bdbda6969b --- /dev/null +++ b/tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.rs @@ -0,0 +1,8 @@ +//@ check-pass +//@ compile-flags: -Zstaticlib-rename-internal-symbols --crate-type bin + +#![feature(no_core)] +#![no_core] +#![no_main] + +//~? WARN has no effect without `--crate-type staticlib` diff --git a/tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.stderr b/tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.stderr new file mode 100644 index 0000000000000..d4095ede81a34 --- /dev/null +++ b/tests/ui/linking/staticlib-rename-internal-symbols-wrong-crate-type.stderr @@ -0,0 +1,2 @@ +warning: -Zstaticlib-rename-internal-symbols has no effect without `--crate-type staticlib` +