Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
727 changes: 724 additions & 3 deletions compiler/rustc_codegen_ssa/src/back/archive.rs

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions compiler/rustc_codegen_ssa/src/back/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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<String> = 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()));
Copy link
Copy Markdown
Contributor

@petrochenkov petrochenkov May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can two short hashes be the same if two different crates have the same name?
I guess not due to the stable_crate_id component of the metadata_symbol?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. StableCrateId incorporates -Cmetadata which Cargo always sets uniquely per crate, so same-name crates get different suffixes. The only way to get a collision is compiling two same-name crates manually without -Cmetadata, but then their mangled symbols are already identical anyway (v0 uses the same StableCrateId as disambiguator).

I'll clean this up to read StableCrateId directly from CrateInfo instead of parsing it out of the metadata symbol string. Is that OK?

let suffix = format!("_rs{}", short_hash);
ab.set_rename_symbols(keep, suffix);
}
}

ab.build(out_filename);

let crates = crate_info.used_crates.iter();
Expand Down
16 changes: 16 additions & 0 deletions compiler/rustc_codegen_ssa/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_interface/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions compiler/rustc_session/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_warn(
"-Zstaticlib-hide-internal-symbols has no effect without `--crate-type staticlib`",
);
}

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 {
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_session/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
Copy link
Copy Markdown
Contributor

@petrochenkov petrochenkov May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference with -Z default-visibility=hidden here?
As far as I can see, that option will also set visibility to hidden for all SymbolExportLevel::Rust symbols.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-Z default-visibility=hidden only affects the current crate's codegen. A staticlib also bundles .o files from upstream crates (std, core, etc.) that were compiled without it. -Zstaticlib-hide-internal-symbols post-processes the entire archive including those pre-compiled upstream objects.


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.
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor

@petrochenkov petrochenkov May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm? It does not?
If it does then why is -Zstaticlib-hide-internal-symbols needed.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, that doc statement is wrong. The default visibility is Interposable (STV_DEFAULT), not hidden. Will fix.

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.
Copy link
Copy Markdown
Contributor

@petrochenkov petrochenkov May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
other crate types will result in a compilation error.
other crate types will result in a compilation warning.

(Same for the hiding flag.)

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.


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.
40 changes: 40 additions & 0 deletions tests/run-make/staticlib-hide-internal-symbols-macho/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![crate_type = "staticlib"]
Copy link
Copy Markdown
Contributor

@petrochenkov petrochenkov May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some test files, e.g. these ones

tests/run-make/staticlib-hide-internal-symbols-macho/lib.rs
tests/run-make/staticlib-hide-internal-symbols/lib.rs
tests/run-make/staticlib-rename-internal-symbols-macho/lib.rs
tests/run-make/staticlib-rename-internal-symbols/lib.rs

are nontrivial but their contents are exactly the same.

Perhaps it's possible to deduplicate the tests to avoid copypasting source files several times?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, will deduplicate.


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,
}
}
18 changes: 18 additions & 0 deletions tests/run-make/staticlib-hide-internal-symbols-macho/main.c
Original file line number Diff line number Diff line change
@@ -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;
}
110 changes: 110 additions & 0 deletions tests/run-make/staticlib-hide-internal-symbols-macho/rmake.rs
Original file line number Diff line number Diff line change
@@ -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<Endianness>;
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<SymbolTable<'data>> {
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::<MachOFileHeader64, _>(endian, data).ok();
}
}
None
}
40 changes: 40 additions & 0 deletions tests/run-make/staticlib-hide-internal-symbols/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
18 changes: 18 additions & 0 deletions tests/run-make/staticlib-hide-internal-symbols/main.c
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading