Skip to content

Commit b480660

Browse files
committed
Auto merge of #155338 - cezarbbb:staticlib-symbol-hygiene, r=<try>
Staticlib hide internal symbols try-job: i686-*
2 parents 3179a47 + e5d8cdf commit b480660

14 files changed

Lines changed: 592 additions & 44 deletions

File tree

compiler/rustc_codegen_ssa/src/back/archive.rs

Lines changed: 204 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
use std::env;
21
use std::error::Error;
32
use std::ffi::OsString;
43
use std::fs::{self, File};
54
use std::io::{self, BufWriter, Write};
65
use std::path::{Path, PathBuf};
6+
use std::{env, mem};
77

88
use ar_archive_writer::{
99
ArchiveKind, COFFShortExport, MachineTypes, NewArchiveMember, write_archive_to_stream,
1010
};
1111
pub use ar_archive_writer::{DEFAULT_OBJECT_READER, ObjectReader};
1212
use object::read::archive::{ArchiveFile, ArchiveKind as ObjectArchiveKind};
13-
use object::read::macho::FatArch;
14-
use rustc_data_structures::fx::FxIndexSet;
13+
use object::read::elf::Sym as _;
14+
use object::read::macho::{FatArch, Nlist};
15+
use object::{Endianness, elf, macho};
16+
use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
1517
use rustc_data_structures::memmap::Mmap;
1618
use rustc_fs_util::TempDirBuilder;
1719
use rustc_metadata::EncodedMetadata;
@@ -318,7 +320,7 @@ pub trait ArchiveBuilder {
318320

319321
fn add_archive(&mut self, archive: &Path, kind: AddArchiveKind<'_>) -> io::Result<()>;
320322

321-
fn build(self: Box<Self>, output: &Path) -> bool;
323+
fn build(self: Box<Self>, output: &Path, exported_symbols: Option<FxHashSet<String>>) -> bool;
322324
}
323325

324326
fn target_archive_format_to_object_kind(format: &str) -> Option<ObjectArchiveKind> {
@@ -401,7 +403,6 @@ enum ArchiveEntrySource {
401403
#[derive(Debug)]
402404
struct ArchiveEntry {
403405
source: ArchiveEntrySource,
404-
#[expect(dead_code)] // used in #155338
405406
kind: ArchiveEntryKind,
406407
}
407408

@@ -534,9 +535,9 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> {
534535

535536
/// Combine the provided files, rlibs, and native libraries into a single
536537
/// `Archive`.
537-
fn build(self: Box<Self>, output: &Path) -> bool {
538+
fn build(self: Box<Self>, output: &Path, exported_symbols: Option<FxHashSet<String>>) -> bool {
538539
let sess = self.sess;
539-
match self.build_inner(output) {
540+
match self.build_inner(output, exported_symbols) {
540541
Ok(any_members) => any_members,
541542
Err(error) => {
542543
sess.dcx().emit_fatal(ArchiveBuildFailure { path: output.to_owned(), error })
@@ -546,7 +547,11 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> {
546547
}
547548

548549
impl<'a> ArArchiveBuilder<'a> {
549-
fn build_inner(self, output: &Path) -> io::Result<bool> {
550+
fn build_inner(
551+
self,
552+
output: &Path,
553+
exported_symbols: Option<FxHashSet<String>>,
554+
) -> io::Result<bool> {
550555
let archive_kind = match &*self.sess.target.archive_format {
551556
"gnu" => ArchiveKind::Gnu,
552557
"bsd" => ArchiveKind::Bsd,
@@ -561,40 +566,51 @@ impl<'a> ArArchiveBuilder<'a> {
561566
let mut entries = Vec::new();
562567

563568
for (entry_name, entry) in self.entries {
564-
let data =
565-
match entry.source {
566-
ArchiveEntrySource::Archive { archive_index, file_range } => {
567-
let src_archive = &self.src_archives[archive_index];
568-
let archive_data = &src_archive.1;
569-
let start = file_range.0 as usize;
570-
let end = start + file_range.1 as usize;
571-
let Some(data) = archive_data.get(start..end) else {
572-
return Err(io_error_context(
573-
"invalid archive member",
574-
io::Error::new(
575-
io::ErrorKind::InvalidData,
576-
format!(
577-
"archive member at offset {start} with size {} \
569+
let data: Box<dyn AsRef<[u8]>> = match entry.source {
570+
ArchiveEntrySource::Archive { archive_index, file_range } => {
571+
let src_archive = &self.src_archives[archive_index];
572+
let archive_data = &src_archive.1;
573+
let start = file_range.0 as usize;
574+
let end = start + file_range.1 as usize;
575+
let Some(data) = archive_data.get(start..end) else {
576+
return Err(io_error_context(
577+
"invalid archive member",
578+
io::Error::new(
579+
io::ErrorKind::InvalidData,
580+
format!(
581+
"archive member at offset {start} with size {} \
578582
exceeds archive size {} in `{}`",
579-
file_range.1,
580-
archive_data.len(),
581-
src_archive.0.display(),
582-
),
583+
file_range.1,
584+
archive_data.len(),
585+
src_archive.0.display(),
583586
),
584-
));
585-
};
586-
587-
Box::new(data) as Box<dyn AsRef<[u8]>>
587+
),
588+
));
589+
};
590+
591+
if entry.kind == ArchiveEntryKind::RustObj
592+
&& let Some(exported) = &exported_symbols
593+
{
594+
Box::new(apply_hide(data, exported))
595+
} else {
596+
Box::new(data)
588597
}
589-
ArchiveEntrySource::File(file) => unsafe {
590-
Box::new(
591-
Mmap::map(File::open(file).map_err(|err| {
592-
io_error_context("failed to open object file", err)
593-
})?)
594-
.map_err(|err| io_error_context("failed to map object file", err))?,
595-
) as Box<dyn AsRef<[u8]>>
596-
},
597-
};
598+
}
599+
ArchiveEntrySource::File(file) => unsafe {
600+
let mmap = Mmap::map(
601+
File::open(file)
602+
.map_err(|err| io_error_context("failed to open object file", err))?,
603+
)
604+
.map_err(|err| io_error_context("failed to map object file", err))?;
605+
if entry.kind == ArchiveEntryKind::RustObj
606+
&& let Some(exported) = &exported_symbols
607+
{
608+
Box::new(apply_hide(&mmap, exported))
609+
} else {
610+
Box::new(mmap) as Box<dyn AsRef<[u8]>>
611+
}
612+
},
613+
};
598614

599615
entries.push(NewArchiveMember {
600616
buf: data,
@@ -655,3 +671,152 @@ impl<'a> ArArchiveBuilder<'a> {
655671
fn io_error_context(context: &str, err: io::Error) -> io::Error {
656672
io::Error::new(io::ErrorKind::Other, format!("{context}: {err}"))
657673
}
674+
675+
// We use the `object` crate for the read-only pass over ELF/Mach-O object files
676+
// because its `Sym`/`Nlist` traits provide clean access to symbol properties without
677+
// manual byte parsing. However, `object` does not expose mutable views into the data,
678+
// so we cannot use it to modify symbol fields in place. Instead, the read-only pass
679+
// collects byte-level patches (offset + new value), and the write pass
680+
// (`apply_patches`) applies them to a copy of the byte buffer without any ELF/Mach-O
681+
// parsing — similar to how linker relocations work.
682+
683+
/// A byte-level patch collected in the read-only pass and applied in the write pass.
684+
struct Patch {
685+
offset: usize,
686+
value: u8,
687+
}
688+
689+
/// Apply a list of byte patches to `data`, returning the (possibly modified) bytes.
690+
fn apply_patches(data: &[u8], patches: &[Patch]) -> Vec<u8> {
691+
let mut buf = data.to_vec();
692+
for p in patches {
693+
buf[p.offset] = p.value;
694+
}
695+
buf
696+
}
697+
698+
// ---------------------------------------------------------------------------
699+
// ELF hide – read-only pass uses `object` crate, write pass uses `Patch` list
700+
// ---------------------------------------------------------------------------
701+
702+
fn elf_hide_patches_impl<'data, Elf: object::read::elf::FileHeader<Endian = Endianness>>(
703+
data: &'data [u8],
704+
st_other_offset: usize,
705+
exported: &FxHashSet<String>,
706+
) -> Option<Vec<Patch>>
707+
where
708+
u64: From<Elf::Word>,
709+
{
710+
let header = Elf::parse(data).ok()?;
711+
let endian = header.endian().ok()?;
712+
let sections = header.sections(endian, data).ok()?;
713+
let symtab = sections.symbols(endian, data, elf::SHT_SYMTAB).ok()?;
714+
715+
let data_ptr = data.as_ptr() as usize;
716+
let strings = symtab.strings();
717+
let mut patches = Vec::new();
718+
719+
for sym in symtab.iter() {
720+
let binding = sym.st_bind();
721+
if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK {
722+
continue;
723+
}
724+
if sym.is_undefined(endian) {
725+
continue;
726+
}
727+
let Ok(name_bytes) = sym.name(endian, strings) else { continue };
728+
let Ok(name) = str::from_utf8(name_bytes) else { continue };
729+
if !exported.contains(name) {
730+
let sym_addr = sym as *const Elf::Sym as usize;
731+
let offset = sym_addr - data_ptr + st_other_offset;
732+
let new_vis = (sym.st_other() & !0x03) | elf::STV_HIDDEN;
733+
patches.push(Patch { offset, value: new_vis });
734+
}
735+
}
736+
737+
Some(patches)
738+
}
739+
740+
// ---------------------------------------------------------------------------
741+
// Mach-O hide – same architecture: read-only pass via `object`, write via patches
742+
// ---------------------------------------------------------------------------
743+
744+
fn macho_hide_patches_impl<'data, Mach: object::read::macho::MachHeader<Endian = Endianness>>(
745+
data: &'data [u8],
746+
n_type_offset: usize,
747+
exported: &FxHashSet<String>,
748+
) -> Option<Vec<Patch>> {
749+
let header = Mach::parse(data, 0).ok()?;
750+
let endian = header.endian().ok()?;
751+
let mut commands = header.load_commands(endian, data, 0).ok()?;
752+
753+
let symtab_cmd = loop {
754+
let cmd = commands.next().ok()??;
755+
if let Some(st) = cmd.symtab().ok().flatten() {
756+
break st;
757+
}
758+
};
759+
let symtab: object::read::macho::SymbolTable<'_, Mach, &_> =
760+
symtab_cmd.symbols(endian, data).ok()?;
761+
762+
let data_ptr = data.as_ptr() as usize;
763+
let strings = symtab.strings();
764+
let mut patches = Vec::new();
765+
766+
for nlist in symtab.iter() {
767+
if nlist.is_stab() {
768+
continue;
769+
}
770+
if nlist.is_undefined() {
771+
continue;
772+
}
773+
if nlist.n_type() & macho::N_EXT == 0 {
774+
continue;
775+
}
776+
let Ok(name_bytes) = nlist.name(endian, strings) else { continue };
777+
let Ok(name) = str::from_utf8(name_bytes) else { continue };
778+
if !exported.contains(name) {
779+
let nlist_addr = nlist as *const Mach::Nlist as usize;
780+
let offset = nlist_addr - data_ptr + n_type_offset;
781+
patches.push(Patch { offset, value: nlist.n_type() | macho::N_PEXT });
782+
}
783+
}
784+
785+
Some(patches)
786+
}
787+
788+
// ---------------------------------------------------------------------------
789+
// Unified dispatch: top-level detection via `object::File::parse`
790+
// ---------------------------------------------------------------------------
791+
792+
fn hide_patches(data: &[u8], exported: &FxHashSet<String>) -> Option<Vec<Patch>> {
793+
let file = object::File::parse(data).ok()?;
794+
match file {
795+
object::File::Elf64(_) => elf_hide_patches_impl::<elf::FileHeader64<Endianness>>(
796+
data,
797+
mem::offset_of!(elf::Sym64<Endianness>, st_other),
798+
exported,
799+
),
800+
object::File::Elf32(_) => elf_hide_patches_impl::<elf::FileHeader32<Endianness>>(
801+
data,
802+
mem::offset_of!(elf::Sym32<Endianness>, st_other),
803+
exported,
804+
),
805+
object::File::MachO64(_) => macho_hide_patches_impl::<macho::MachHeader64<Endianness>>(
806+
data,
807+
mem::offset_of!(macho::Nlist64<Endianness>, n_type),
808+
exported,
809+
),
810+
object::File::MachO32(_) => macho_hide_patches_impl::<macho::MachHeader32<Endianness>>(
811+
data,
812+
mem::offset_of!(macho::Nlist32<Endianness>, n_type),
813+
exported,
814+
),
815+
_ => None,
816+
}
817+
}
818+
819+
fn apply_hide(data: &[u8], exported: &FxHashSet<String>) -> Vec<u8> {
820+
let patches = hide_patches(data, exported).unwrap_or_default();
821+
apply_patches(data, &patches)
822+
}

compiler/rustc_codegen_ssa/src/back/link.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub fn link_binary(
131131
RlibFlavor::Normal,
132132
&path,
133133
)
134-
.build(&out_filename);
134+
.build(&out_filename, None);
135135
}
136136
CrateType::StaticLib => {
137137
link_staticlib(
@@ -566,7 +566,22 @@ fn link_staticlib(
566566
sess.dcx().emit_fatal(e);
567567
}
568568

569-
ab.build(out_filename);
569+
let exported_symbols = if sess.opts.unstable_opts.staticlib_hide_internal_symbols {
570+
if !matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO) {
571+
sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported {
572+
binary_format: sess.target.archive_format.to_string(),
573+
});
574+
None
575+
} else {
576+
crate_info
577+
.exported_symbols
578+
.get(&CrateType::StaticLib)
579+
.map(|symbols| symbols.iter().map(|(s, _)| s.clone()).collect())
580+
}
581+
} else {
582+
None
583+
};
584+
ab.build(out_filename, exported_symbols);
570585

571586
let crates = crate_info.used_crates.iter();
572587

@@ -1265,7 +1280,7 @@ fn link_natively(
12651280
if should_archive {
12661281
let mut ab = archive_builder_builder.new_archive_builder(sess);
12671282
ab.add_file(temp_filename, ArchiveEntryKind::Other);
1268-
ab.build(out_filename);
1283+
ab.build(out_filename, None);
12691284
}
12701285
}
12711286

@@ -3265,7 +3280,7 @@ fn add_static_crate(
32653280
sess.dcx()
32663281
.emit_fatal(errors::RlibArchiveBuildFailure { path: cratepath.clone(), error });
32673282
}
3268-
if archive.build(&dst) {
3283+
if archive.build(&dst, None) {
32693284
link_upstream(&dst);
32703285
}
32713286
});

compiler/rustc_codegen_ssa/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,14 @@ pub(crate) struct IncompatibleArchiveFormat {
693693
#[diag("linking static libraries is not supported for BPF")]
694694
pub(crate) struct BpfStaticlibNotSupported;
695695

696+
#[derive(Diagnostic)]
697+
#[diag(
698+
"-Zstaticlib-hide-internal-symbols only supports ELF and Mach-O targets, but the target uses `{$binary_format}`"
699+
)]
700+
pub(crate) struct StaticlibHideInternalSymbolsUnsupported {
701+
pub binary_format: String,
702+
}
703+
696704
#[derive(Diagnostic)]
697705
#[diag("entry symbol `main` declared multiple times")]
698706
#[help(

compiler/rustc_interface/src/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ fn test_unstable_options_tracking_hash() {
866866
tracked!(split_lto_unit, Some(true));
867867
tracked!(src_hash_algorithm, Some(SourceFileHashAlgorithm::Sha1));
868868
tracked!(stack_protector, StackProtector::All);
869+
tracked!(staticlib_hide_internal_symbols, true);
869870
tracked!(teach, true);
870871
tracked!(thinlto, Some(true));
871872
tracked!(tiny_const_eval_limit, true);

compiler/rustc_interface/src/util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ impl ArchiveBuilderBuilder for DummyArchiveBuilderBuilder {
471471
output_path: &Path,
472472
) {
473473
// Build an empty static library to avoid calling an external dlltool on mingw
474-
ArArchiveBuilderBuilder.new_archive_builder(sess).build(output_path);
474+
ArArchiveBuilderBuilder.new_archive_builder(sess).build(output_path, None);
475475
}
476476
}
477477

compiler/rustc_session/src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2465,6 +2465,14 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
24652465
let mut collected_options = Default::default();
24662466

24672467
let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options);
2468+
2469+
if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::StaticLib)
2470+
{
2471+
early_dcx.early_warn(
2472+
"-Zstaticlib-hide-internal-symbols has no effect without `--crate-type staticlib`",
2473+
);
2474+
}
2475+
24682476
let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(early_dcx, matches);
24692477

24702478
if !unstable_opts.unstable_options && json_timings {

0 commit comments

Comments
 (0)