Skip to content

Don't leak non-exported symbols from staticlibs #104707

Open

Description

@bjorn3

View all comments

When compiling a cdylib only #[no_mangle] symbols are exported. #[rustc_std_internal_symbol] and mangled symbols are not exported. This prevents symbol conflicts and avoids overriding symbols in ways that causes UB when using a rust cdylib in a rust program. For staticlibs however all symbols leak out of the staticlib. Causing symbol overrides that are potentially UB and symbols conflicts. For example when statically linking spidermonkey, you will get symbol conflicts if the rust version you compile your program with doesn't match the one the rust parts of spidermonkey were compiled with.

cc rust-lang/wg-allocators#108 (comment)
cc https://bytecodealliance.zulipchat.com/#narrow/stream/206238-general/topic/.E2.9C.94.20spidermonkey-wasm-rs/near/309604553

Activity

  1. added
    A-linkageArea: linking into static, shared libraries and binaries
    on Nov 22, 2022
  2. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor
  3. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor

    FTR, I'm unsure there is a way to limit exported symbols from a staticlib, without both limiting the number of CGUs within the crate to 1 and preventing any upstream crates from using the #[global_allocator]. There is further limited ways to prevent upstream crate symbols from being exported. My Original Post was made with that in mind.

  4. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor

    Maybe partial linking would work? If not we will at least need to version all #[rustc_std_internal_symbol]s for each rustc version to prevent UB if different rustc versions implement them in backwards incompatible ways. And maybe we can edit every object file to rename symbols to be unique? By the way note that for cdylib and staticlib all rust crate dependencies are included directly in the cdylib/staticlib. No dynamic linking supported.

  5. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor

    Maybe partial linking would work?

    IDK whether all linkers support partial linking (in particular, I don't know about microsoft's link.exe). Though, personally, I don't particularily want to touch ld -r with a 10' pole anyways if I don't have to. The versioning would work, if done within the mangling (for mangled symbols).

    By the way note that for cdylib and staticlib all rust crate dependencies are included directly in the cdylib/staticlib. No dynamic linking supported.

    I plan to do it slightly differently when possible - for staticlibs resolve the dependencies and produce a links.o "object" that is just a linker script. For cdylibs, just link as normal and when dynamically linking, add DT_RPATH (to the stdlib directory)+DT_NEEDED as needed.

  6. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor

    for staticlibs resolve the dependencies and produce a links.o "object" that is just a linker script.

    That is a backward incompatible change for rustc

    For cdylibs, just link as normal and when dynamically linking, add DT_RPATH (to the stdlib directory)+DT_NEEDED as needed.

    That will share and thus leak the global state of libstd across the cdylib boundary. This among other things will break the mitigation of #102721 to prevent catching foreign rust panics.

  7. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor
  8. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor

    rustc currently just emits the library directly into the archive, right?
    I'm not particularly sure what the difference here is, except in terms of
    file size, and the fact that using libstd.so.0.1 is an option in addition
    to libstd.rlib.0.1.

    The difference is that currently you can ship a staticlib as a standalone file and expect linking to succeed, but with your proposal you also need all dependencies to be available at exactly the same place.

    That is true, though I'm unsure how it is avoidable - even when statically
    linking the symbols from libstd et. al need to have default visibility so
    that when linking into a dylib, the symbols can be used via the dylib. It
    would not make a difference on ELF.

    When linking a cdylib, libstd is statically linked and none of it's symbols are exported from the cdylib. When linking a rust dylib, sharing state is just fine. In fact you aren't allowed to duplicate crates in that case.

  9. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor

    The difference is that currently you can ship a staticlib as a standalone file and expect linking to succeed, but with your proposal you also need all dependencies to be available at exactly the same place.

    Fair enough, though the result is potentially shipping GiB for an API surface that should take MiB.

    When linking a cdylib, libstd is statically linked and none of it's symbols are exported from the cdylib. When linking a rust dylib, sharing state is just fine. In fact you aren't allowed to duplicate crates in that case.

    On ELF I'm unsure how this would be achieved. Symbol visibilty is controlled when the symbol is defined (in the object file), and I just send everything on to ld (post processing? What is that? I only know "add head and tail libraries to link line"). ELF shared objects don't have an "Export List", the dynamic symbol list is just built from the static symbol list usually excluding internal and hidden symbols. Every GLOBAL or WEAK symbol in the list with PROTECTED or DEFAULT visibility can be imported from the cdylib and every symbol with DEFAULT visibilty can additonally be replaced. These are functions of the link editor producing the files and the dynamic linker-loader resolving runtime relocations, and are far from under the control of rust as a language or any particular implementation.

  10. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor

    Rustc passes a version script to the linker specifying exactly which symbols to export and making it to hide everything else. This is but one of the reasons rustc is in change of invoking the linker.

  11. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor

    Rustc passes a version script to the linker specifying exactly which symbols to export and making it to hide everything else. This is but one of the reasons rustc is in change of invoking the linker.

    Ah. My problem is that I have to support link editors that are like "Version Script? What is this? Expected PHDRS, MEMORY, or SECTIONS"

  12. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor

    Linker scripts and versions scripts are different. Linker scripts tell what should be put where in the linked artifact. Version scripts only list which symbols are exported and which aren't amd optionally provide a version for the purpose of symbol versioning. The format of version scripts is trivial in comparison to linker scripts. See

    if self.sess.target.is_like_osx {
    // Write a plain, newline-separated list of symbols
    let res: io::Result<()> = try {
    let mut f = BufWriter::new(File::create(&path)?);
    for sym in symbols {
    debug!(" _{}", sym);
    writeln!(f, "_{}", sym)?;
    }
    };
    if let Err(error) = res {
    self.sess.emit_fatal(errors::LibDefWriteFailure { error });
    }
    } else if is_windows {
    let res: io::Result<()> = try {
    let mut f = BufWriter::new(File::create(&path)?);
    // .def file similar to MSVC one but without LIBRARY section
    // because LD doesn't like when it's empty
    writeln!(f, "EXPORTS")?;
    for symbol in symbols {
    debug!(" _{}", symbol);
    writeln!(f, " {}", symbol)?;
    }
    };
    if let Err(error) = res {
    self.sess.emit_fatal(errors::LibDefWriteFailure { error });
    }
    } else {
    // Write an LD version script
    let res: io::Result<()> = try {
    let mut f = BufWriter::new(File::create(&path)?);
    writeln!(f, "{{")?;
    if !symbols.is_empty() {
    writeln!(f, " global:")?;
    for sym in symbols {
    debug!(" {};", sym);
    writeln!(f, " {};", sym)?;
    }
    }
    writeln!(f, "\n local:\n *;\n}};")?;
    };
    if let Err(error) = res {
    self.sess.emit_fatal(errors::VersionScriptWriteFailure { error });
    }
    }
    if self.sess.target.is_like_osx {
    self.linker_args(&[OsString::from("-exported_symbols_list"), path.into()]);
    } else if self.sess.target.is_like_solaris {
    self.linker_args(&[OsString::from("-M"), path.into()]);
    } else {
    if is_windows {
    self.linker_arg(path);
    } else {
    let mut arg = OsString::from("--version-script=");
    arg.push(path);
    self.linker_arg(arg);
    }
    }

  13. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor

    I am aware of version scripts. The simplicity is not the problem. The problem is if I'm faced with a link editor that doesn't support them, which I cannot always assume.

  14. bjorn3 commented on Nov 22, 2022

    @bjorn3
    MemberAuthor

    Which linker doesn't support it? AFAIK every platform targeted by rustc has a linker supporting them or some other way to hide symbols.

  15. chorman0773 commented on Nov 22, 2022

    @chorman0773
    Contributor

    Hmm... I'm actually not sure. Some quick research on autoconf says only GNU ld + solaris LD (and lld supports it, as will lcld). I guess older platforms may have none of the above, but IDK how old you have to get. I'm sure given enough time I could find a counterexample, but I don't want to look rn.

  16. 52 remaining items

  17. added a commit that references this issue on Jun 5, 2026
  18. added a commit that references this issue on Jun 5, 2026
  19. added 2 commits that reference this issue on Jun 5, 2026
  20. added a commit that references this issue on Jun 8, 2026
  21. steckes commented on Sep 15, 2026

    @steckes

    We are shipping a pre-compiled Rust library that some of our clients want to use again from Rust. The leaked symbols were causing issues, so we tried @zeozeozeo's tool (https://github.com/zeozeozeo/staticlib-fucker) to hide them.
    As we are shipping our library across so many different targets, we needed to come up with a cross platform solution. After working on it for nearly a year and shipping all of our libraries patched with this tool, we are confident that it works well across all of our targets.

    So we decided to open source it!
    Please use of it and let us know if you hit any limitations or bugs 🙂

    https://github.com/ai-coustics/lib-patcher

  22. decentral1se commented on Sep 15, 2026

    @decentral1se

    Amazing @steckes, you're all heroes. I will be sure to test this out in the coming days 🙏

  23. apiraino commented on Sep 15, 2026

    @apiraino
    Contributor

    rust-mod action: some messages were temporarily marked as possible adverstisement. Feel free to get in contact for a review.

  24. bjorn3 commented on Sep 15, 2026

    @bjorn3
    MemberAuthor

    Since #155338 we have the unstable -Zstaticlib-hide-internal-symbols option to prevent exporting symbols from dylibs (ELF and Mach-O only) and since #156950 we have -Zstaticlib-rename-internal-symbols to rename symbols to prevent conflicts (ELF and Mach-O currently, #160679 will add PE/COFF support).

  25. steckes commented on Sep 15, 2026

    @steckes

    Yes and that is is amazing, can't wait for this to not be an issue anymore! :)

  26. decentral1se commented on Oct 11, 2026

    @decentral1se

    I tested #104707 (comment) and it solved the problems I am facing: https://git.coopcloud.tech/decentral1se/p2panda-loro-go

    Thank you all very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-linkageArea: linking into static, shared libraries and binariesC-enhancementCategory: An issue proposing an enhancement or a PR with one.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions