diff --git a/compiler/rustc_codegen_ssa/src/back/archive.rs b/compiler/rustc_codegen_ssa/src/back/archive.rs index ba811825dce23..83cdc1e3c6062 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, ArchiveKind as ObjectArchiveKind}; 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); } fn target_archive_format_to_object_kind(format: &str) -> Option { @@ -381,6 +386,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)] @@ -391,7 +398,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)); } } @@ -520,6 +542,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> { @@ -537,8 +567,57 @@ 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 { + let d = data.as_ref().as_ref(); + elf_collect_rename_set(d, keep, &mut all_names); + macho_collect_rename_set(d, 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]; @@ -573,6 +652,38 @@ impl<'a> ArArchiveBuilder<'a> { }, }; + let data: Box> = if let Some(ref internal_set) = global_internal_set { + if should_rename { + let suffix = rename_suffix.as_ref().unwrap(); + if let Some(renamed) = + elf_apply_rename(data.as_ref().as_ref(), internal_set, suffix, should_hide) + { + Box::new(renamed) + } else if let Some(renamed) = macho_apply_rename( + data.as_ref().as_ref(), + internal_set, + suffix, + should_hide, + ) { + Box::new(renamed) + } else { + data + } + } else { + if let Some(hidden) = elf_apply_hide(data.as_ref().as_ref(), internal_set) { + Box::new(hidden) + } else if let Some(hidden) = + macho_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, @@ -632,3 +743,608 @@ 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); +} + +/// Parsed Mach-O symbol table information from an MH_OBJECT file. +struct MachOSymtab<'a> { + endian: Endianness, + /// File offset of the LC_SYMTAB load command. + symtab_cmd_offset: usize, + /// File offset of the nlist array. + sym_offset: usize, + /// Number of nlist entries. + nsyms: usize, + /// String table data. + strtab_data: &'a [u8], + /// Size of one nlist entry (12 for 32-bit, 16 for 64-bit). + nlist_size: usize, +} + +impl<'a> MachOSymtab<'a> { + fn nlist_off(&self, i: usize) -> usize { + self.sym_offset + i * self.nlist_size + } + + fn n_strx(&self, data: &[u8], i: usize) -> u32 { + let off = self.nlist_off(i); + let bytes: [u8; 4] = data[off..off + 4].try_into().unwrap_or([0; 4]); + match self.endian { + Endianness::Little => u32::from_le_bytes(bytes), + Endianness::Big => u32::from_be_bytes(bytes), + } + } + + fn n_type(&self, data: &[u8], i: usize) -> u8 { + data[self.nlist_off(i) + 4] + } + + fn is_external(&self, data: &[u8], i: usize) -> bool { + use object::macho; + let t = self.n_type(data, i); + t & macho::N_STAB == 0 && t & macho::N_EXT != 0 + } + + fn is_external_defined(&self, data: &[u8], i: usize) -> bool { + use object::macho; + let t = self.n_type(data, i); + t & macho::N_STAB == 0 && t & macho::N_EXT != 0 && t & macho::N_TYPE == macho::N_SECT + } + + fn read_name(&self, data: &[u8], i: usize) -> Option { + let strx = self.n_strx(data, i) as usize; + if strx >= self.strtab_data.len() { + return None; + } + let end = self.strtab_data[strx..] + .iter() + .position(|&b| b == 0) + .unwrap_or(self.strtab_data.len() - strx); + std::str::from_utf8(&self.strtab_data[strx..strx + end]).ok().map(|s| s.to_string()) + } +} + +/// Parse a Mach-O object file and extract symbol table info. +fn macho_symtab_info(data: &[u8]) -> Option> { + use object::macho; + + if data.len() < 8 { + return None; + } + + let magic = u32::from_ne_bytes(data[0..4].try_into().ok()?); + let (is_64, is_big_endian) = match magic { + macho::MH_MAGIC => (false, false), + macho::MH_MAGIC_64 => (true, false), + macho::MH_CIGAM => (false, true), + macho::MH_CIGAM_64 => (true, true), + _ => return None, + }; + + let endian = if is_big_endian { Endianness::Big } else { Endianness::Little }; + let nlist_size = if is_64 { 16 } else { 12 }; + let header_size = if is_64 { 32usize } else { 28 }; + + let read_u32 = |off: usize| -> Option { + let bytes: [u8; 4] = data.get(off..off + 4)?.try_into().ok()?; + Some(match endian { + Endianness::Little => u32::from_le_bytes(bytes), + Endianness::Big => u32::from_be_bytes(bytes), + }) + }; + + let ncmds = read_u32(16)? as usize; + let sizeofcmds = read_u32(20)? as usize; + + if header_size + sizeofcmds > data.len() { + return None; + } + + // Iterate load commands to find LC_SYMTAB + let mut cmd_offset = header_size; + for _ in 0..ncmds { + if cmd_offset + 8 > header_size + sizeofcmds { + break; + } + let cmd = read_u32(cmd_offset)?; + let cmdsize = read_u32(cmd_offset + 4)? as usize; + if cmdsize < 8 { + break; + } + + if cmd == macho::LC_SYMTAB { + if cmd_offset + 24 > data.len() { + return None; + } + let symoff = read_u32(cmd_offset + 8)? as usize; + let nsyms = read_u32(cmd_offset + 12)? as usize; + let stroff = read_u32(cmd_offset + 16)? as usize; + let strsize = read_u32(cmd_offset + 20)? as usize; + + if symoff + nsyms * nlist_size > data.len() { + return None; + } + if stroff + strsize > data.len() { + return None; + } + + return Some(MachOSymtab { + endian, + symtab_cmd_offset: cmd_offset, + sym_offset: symoff, + nsyms, + strtab_data: &data[stroff..stroff + strsize], + nlist_size, + }); + } + + cmd_offset += cmdsize; + } + None +} + +/// Collect defined external symbol names from a Mach-O object that are NOT in +/// `keep_symbols`. +fn macho_collect_rename_set( + data: &[u8], + keep_symbols: &FxHashSet, + out_set: &mut FxHashSet, +) { + let Some(tab) = macho_symtab_info(data) else { return }; + for i in 0..tab.nsyms { + if !tab.is_external_defined(data, i) { + continue; + } + if let Some(name) = tab.read_name(data, i) { + let lookup = name.strip_prefix('_').unwrap_or(&name); + if !keep_symbols.contains(lookup) { + out_set.insert(name); + } + } + } +} + +/// For Mach-O object files, hide external defined symbols whose names are in +/// `hide_set` by setting the `N_PEXT` bit on their `n_type` field. +fn macho_apply_hide(data: &[u8], hide_set: &FxHashSet) -> Option> { + use object::macho; + + let tab = macho_symtab_info(data)?; + if tab.nsyms == 0 { + return None; + } + + let mut result: Option> = None; + for i in 0..tab.nsyms { + if !tab.is_external_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()); + let n_type_off = tab.nlist_off(i) + 4; + buf[n_type_off] |= macho::N_PEXT; + } + } + } + result +} + +/// Appends a new string table to the end of the file and patches +/// `LC_SYMTAB.stroff`/`strsize` in place. +fn macho_apply_rename( + data: &[u8], + rename_set: &FxHashSet, + suffix: &str, + hide: bool, +) -> Option> { + use object::macho; + + let tab = macho_symtab_info(data)?; + if tab.nsyms == 0 { + return None; + } + + // Collect matched names from this file (external symbols, defined or not) + let mut matched_names: FxHashSet = FxHashSet::default(); + for i in 0..tab.nsyms { + if !tab.is_external(data, i) { + 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; + } + + // Build new strtab: original content + appended renamed entries + 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); + } + + // Layout: [original file 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 + write_u32_at(&mut result, tab.symtab_cmd_offset + 16, new_strtab_file_off as u32, tab.endian); + write_u32_at(&mut result, tab.symtab_cmd_offset + 20, new_strtab_size as u32, tab.endian); + + // Patch nlist entries + for i in 0..tab.nsyms { + if !tab.is_external(data, i) { + continue; + } + if let Some(name) = tab.read_name(data, i) { + if let Some(&new_strx) = rename_map.get(&name) { + let off = tab.nlist_off(i); + write_u32_at(&mut result, off, new_strx, tab.endian); + if hide && tab.is_external_defined(data, i) { + result[off + 4] |= macho::N_PEXT; + } + } + } + } + + Some(result) +} diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index d8e2b54ad50a8..e502e5aac0a0f 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -555,6 +555,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" | "darwin") { + 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" | "darwin") { + 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 cceda09c701cc..9aa533efcc140 100644 --- a/compiler/rustc_codegen_ssa/src/errors.rs +++ b/compiler/rustc_codegen_ssa/src/errors.rs @@ -693,6 +693,22 @@ pub(crate) struct IncompatibleArchiveFormat { #[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 a7e0dd2ac39c0..b436da69f36e9 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 f8da18632a997..f1f923a602737 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -2471,6 +2471,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 4767b0cfbe868..d70c11bb27892 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2658,6 +2658,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-macho/lib.rs b/tests/run-make/staticlib-hide-internal-symbols-macho/lib.rs new file mode 100644 index 0000000000000..4bbf21bf1918a --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols-macho/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-macho/main.c b/tests/run-make/staticlib-hide-internal-symbols-macho/main.c new file mode 100644 index 0000000000000..580c805fed158 --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols-macho/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-macho/rmake.rs b/tests/run-make/staticlib-hide-internal-symbols-macho/rmake.rs new file mode 100644 index 0000000000000..5c19e51fb69e6 --- /dev/null +++ b/tests/run-make/staticlib-hide-internal-symbols-macho/rmake.rs @@ -0,0 +1,110 @@ +//@ 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::{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 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 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); + + let exported = EXPORTED.contains(&name); + let has_pext = n_type & N_PEXT != 0; + + if with_flag { + if exported { + assert!(!has_pext, "with -Z hide: exported `{name}` should NOT have N_PEXT"); + } else { + assert!(has_pext, "with -Z hide: internal `{name}` should have N_PEXT"); + } + } else if exported { + assert!(!has_pext, "without -Z: exported `{name}` should NOT have N_PEXT"); + } + + 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 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-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-macho/dual_main.c b/tests/run-make/staticlib-rename-internal-symbols-macho/dual_main.c new file mode 100644 index 0000000000000..21f4d5cae9b55 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/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-macho/lib.rs b/tests/run-make/staticlib-rename-internal-symbols-macho/lib.rs new file mode 100644 index 0000000000000..4bbf21bf1918a --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/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-macho/liba.rs b/tests/run-make/staticlib-rename-internal-symbols-macho/liba.rs new file mode 100644 index 0000000000000..ca23944df44ea --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/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-macho/libb.rs b/tests/run-make/staticlib-rename-internal-symbols-macho/libb.rs new file mode 100644 index 0000000000000..1eca8f3d254ca --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/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-macho/main.c b/tests/run-make/staticlib-rename-internal-symbols-macho/main.c new file mode 100644 index 0000000000000..580c805fed158 --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/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-macho/rmake.rs b/tests/run-make/staticlib-rename-internal-symbols-macho/rmake.rs new file mode 100644 index 0000000000000..ba71f77cb58da --- /dev/null +++ b/tests/run-make/staticlib-rename-internal-symbols-macho/rmake.rs @@ -0,0 +1,151 @@ +//@ 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::{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() { + 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..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 2fe1657e7ecf2..275bf90e2554a 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1288,6 +1288,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` +