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
451 changes: 448 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 @@ -525,6 +525,38 @@ fn link_staticlib(
sess.dcx().emit_fatal(e);
}

if sess.opts.unstable_opts.staticlib_hide_internal_symbols {
if !matches!(&*sess.target.archive_format, "gnu" | "bsd") {
sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported {
archive_format: sess.target.archive_format.to_string(),
});
} else if let Some(symbols) = crate_info.exported_symbols.get(&CrateType::StaticLib) {
use rustc_data_structures::fx::FxHashSet;
let keep: FxHashSet<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") {
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()));
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 @@ -675,6 +675,22 @@ pub(crate) struct UnknownArchiveKind<'a> {
#[diag("linking static libraries is not supported for BPF")]
pub(crate) struct BpfStaticlibNotSupported;

#[derive(Diagnostic)]
#[diag(
"-Zstaticlib-hide-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`"
)]
pub(crate) struct StaticlibHideInternalSymbolsUnsupported {
pub archive_format: String,
}

#[derive(Diagnostic)]
#[diag(
"-Zstaticlib-rename-internal-symbols only supports ELF archive formats (gnu/bsd), but the target uses `{$archive_format}`"
)]
pub(crate) struct StaticlibRenameInternalSymbolsUnsupported {
pub archive_format: String,
}

#[derive(Diagnostic)]
#[diag("entry symbol `main` declared multiple times")]
#[help(
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 @@ -2473,6 +2473,22 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
let mut collected_options = Default::default();

let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options);

if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::StaticLib)
{
early_dcx.early_fatal(
"-Zstaticlib-hide-internal-symbols can only be used with `--crate-type staticlib`",
);
}

if unstable_opts.staticlib_rename_internal_symbols
&& !crate_types.contains(&CrateType::StaticLib)
{
early_dcx.early_fatal(
"-Zstaticlib-rename-internal-symbols can only be used with `--crate-type staticlib`",
);
}

let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(early_dcx, matches);

if !unstable_opts.unstable_options && json_timings {
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 @@ -2764,6 +2764,10 @@ written to standard error output)"),
"control stack smash protection strategy (`rustc --print stack-protector-strategies` for details)"),
staticlib_allow_rdylib_deps: bool = (false, parse_bool, [TRACKED],
"allow staticlibs to have rust dylib dependencies"),
staticlib_hide_internal_symbols: bool = (false, parse_bool, [TRACKED],
"hide non-exported symbols in ELF static libraries by setting STV_HIDDEN"),
staticlib_rename_internal_symbols: bool = (false, parse_bool, [TRACKED],
"rename Rust internal symbols when building staticlibs to avoid conflicts"),
staticlib_prefer_dynamic: bool = (false, parse_bool, [TRACKED],
"prefer dynamic linking to static linking for staticlibs (default: no)"),
strict_init_checks: bool = (false, parse_bool, [TRACKED],
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`.

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
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.
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;
}
132 changes: 132 additions & 0 deletions tests/run-make/staticlib-hide-internal-symbols/rmake.rs
Original file line number Diff line number Diff line change
@@ -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<Endianness>;
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, &sections, 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<Endianness>,
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",
}
}
14 changes: 14 additions & 0 deletions tests/run-make/staticlib-rename-internal-symbols/dual_main.c
Original file line number Diff line number Diff line change
@@ -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;
}
40 changes: 40 additions & 0 deletions tests/run-make/staticlib-rename-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,
}
}
Loading
Loading