Skip to content

Unable to link code containing naked functions on ARM64 macOS with ThinLTO enabled #148307

Closed

Description

@mkaput

I have originally reported this issue in wasmtime repository and was redirected here: bytecodealliance/wasmtime#11957. I have tried to minimize the reproduction, but I failed to do so in reasonable time. The complexity of wasmtime crates is critical here. I would appreciate any hints that would help me minimize this.

Compiling Wasmtime 38.0.3 produces a linker error on ARM64 macOS when building with ThinLTO enabled. The error indicates that wasmtime_fiber_switch symbol is undefined during linking.

error: linking with `cc` failed: exit status: 1
  |
  = note:  "cc" "<426 object files omitted>" "/var/folders/8c/r_zrp9mj12xd414t08zrks940000gn/T/rustc2wYYXA/{libwasmtime-e843f7bbc21927d3,libzstd_sys-7eabaf414ee67d02,libwasmtime_internal_jit_debug-9043bb9a4e496da8}.rlib" "<sysroot>/lib/rustlib/aarch64-apple-darwin/lib/libcompiler_builtins-*.rlib" "-liconv" "-lSystem" "-lc" "-lm" "-arch" "arm64" "-mmacosx-version-min=11.0.0" "-L" "/Users/mk/Developer/software-mansion/scarb/linker-repro/wasmtime-only-test/target/release/build/wasmtime-15671f59b24bbe0f/out" "-L" "/Users/mk/Developer/software-mansion/scarb/linker-repro/wasmtime-only-test/target/release/build/zstd-sys-1ea01473940232bc/out" "-L" "/Users/mk/Developer/software-mansion/scarb/linker-repro/wasmtime-only-test/target/release/build/wasmtime-internal-jit-debug-2aaf81cce8508d62/out" "-o" "/Users/mk/Developer/software-mansion/scarb/linker-repro/wasmtime-only-test/target/release/deps/wasmtime_only_test-69cba5150eff8962" "-Wl,-dead_strip" "-nodefaultlibs"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: ld: warning: object file (/private/var/folders/8c/r_zrp9mj12xd414t08zrks940000gn/T/rustc2wYYXA/libwasmtime-e843f7bbc21927d3.rlib[2](242b1992def1ef0b-helpers.o)) was built for newer 'macOS' version (26.0) than being linked (11.0)
          ld: warning: object file (/private/var/folders/8c/r_zrp9mj12xd414t08zrks940000gn/T/rustc2wYYXA/libwasmtime_internal_jit_debug-9043bb9a4e496da8.rlib[2](db3b6bfb95261072-gdbjit.o)) was built for newer 'macOS' version (26.0) than being linked (11.0)
          Undefined symbols for architecture arm64:
            "wasmtime_internal_fiber::stackswitch::aarch64::wasmtime_fiber_switch::h0b241ee887e10750", referenced from:
                wasmtime_internal_fiber::unix::Suspend::switch::hfd4514b4efef3e13 in wasmtime_only_test-69cba5150eff8962.wasmtime-e843f7bbc21927d3.wasmtime.7e3b8021754df0c4-cgu.05.rcgu.o.rcgu.o
          ld: symbol(s) not found for architecture arm64
          clang: error: linker command failed with exit code 1 (use -v to see invocation)
          

What's important is that this issue only pops up with ThinLTO enabled. It didn't fail with either LTO disabled, set to FatLTO, or Wasmtime 37.

Steps to Reproduce

  • Architecture: ARM64 (Apple Silicon)
  • OS: macOS 26.0
  • Rust: 1.90.0
  • Wasmtime: 38.0.3
  • Build Profile: release with lto = "thin"

Cargo.toml:

[package]
name = "linker-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
wasmtime = "38"

[profile.release]
lto = "thin"

src/main.rs:

fn main() {
    let engine = wasmtime::Engine::default();
    println!("Engine created: {:?}", engine);
}

Build command:

cargo build --release

This will fail with the linker error shown above.

I think the wasmtime commit that triggers this regression is: bytecodealliance/wasmtime@3e9eca8

Meta

I have tested this reproduction on all stable+beta+nightly at the time of submitting this issue:

rustc --version --verbose:

rustc 1.90.0 (1159e78c4 2025-09-14)
binary: rustc
commit-hash: 1159e78c4747b02ef996e55082b704c09b970588
commit-date: 2025-09-14
host: aarch64-apple-darwin
release: 1.90.0
LLVM version: 20.1.8
rustc 1.92.0-beta.1 (3b4dd9bf1 2025-10-28)
binary: rustc
commit-hash: 3b4dd9bf1410f8da6329baa36ce5e37673cbbd1f
commit-date: 2025-10-28
host: aarch64-apple-darwin
release: 1.92.0-beta.1
LLVM version: 21.1.3
rustc 1.93.0-nightly (292be5c7c 2025-10-29)
binary: rustc
commit-hash: 292be5c7c05138d753bbd4b30db7a3f1a5c914f7
commit-date: 2025-10-29
host: aarch64-apple-darwin
release: 1.93.0-nightly
LLVM version: 21.1.3

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 30, 2025
  2. added
    A-nakedArea: `#[naked]`, prologue and epilogue-free, functions, https://git.io/vAzzS
    A-LTOArea: Link-time optimization (LTO)
    A-linkageArea: linking into static, shared libraries and binaries
    on Oct 30, 2025
  3. folkertdev commented on Oct 31, 2025

    @folkertdev
    Contributor

    I extracted the before and after version of the offending function here https://godbolt.org/z/5a16njzE8

            .section        __TEXT,__text,regular,pure_instructions
    
            .p2align        4
            .private_extern _wasmtime_fiber_switch
            .globl  _wasmtime_fiber_switch
    _wasmtime_fiber_switch:
    

    becomes

            .section        __TEXT,__text,regular,pure_instructions
            .p2align        2
    example::wasmtime_fiber_switch_new::h48b3ba9107b841cf:
    

    The .globl and .private_extern lines have disappeared. As you can see, these were added specifically by the old asm_func! macro. Experimentally, either is sufficient to make the example link correctly.


    a quick fix is to make this function #[unsafe(no_mangle)]: that will emit the .globl.


    We do have code to emit .globl on macos, here:

    let write_linkage = |w: &mut String| -> std::fmt::Result {
    match item_data.linkage {
    Linkage::External => {
    writeln!(w, ".globl {asm_name}")?;
    }
    Linkage::LinkOnceAny | Linkage::LinkOnceODR | Linkage::WeakAny | Linkage::WeakODR => {
    match asm_binary_format {
    BinaryFormat::Elf | BinaryFormat::Coff | BinaryFormat::Wasm => {
    writeln!(w, ".weak {asm_name}")?;
    }
    BinaryFormat::Xcoff => {
    // FIXME: there is currently no way of defining a weak symbol in inline assembly
    // for AIX. See https://github.com/llvm/llvm-project/issues/130269
    emit_fatal(
    "cannot create weak symbols from inline assembly for this target",
    )
    }
    BinaryFormat::MachO => {
    writeln!(w, ".globl {asm_name}")?;
    writeln!(w, ".weak_definition {asm_name}")?;
    }
    }
    }
    Linkage::Internal => {
    // write nothing
    }
    Linkage::Common => emit_fatal("Functions may not have common linkage"),
    Linkage::AvailableExternally => {
    // this would make the function equal an extern definition
    emit_fatal("Functions may not have available_externally linkage")
    }
    Linkage::ExternalWeak => {
    // FIXME: actually this causes a SIGILL in LLVM
    emit_fatal("Functions may not have external weak linkage")
    }
    }
    Ok(())
    };

    So somehow the linkage is such that this does not fire, I think? It seems plausible that it is Linkage::Internal, maybe on macos we should emit a directive even then?

  4. folkertdev commented on Oct 31, 2025

    @folkertdev
    Contributor

    I suspect that the fix here is to emit .private_extern {asm_name} when the linkage is Linkage::Internal.

  5. bjorn3 commented on Oct 31, 2025

    @bjorn3
    Member

    I think what happens is that rustc sees that the naked function is only callable from functions within the same codegen unit and thus marks the naked function as private, but then ThinLTO decides to inline the caller into another codegen unit. Normally it would make the callee public, but it can't for symbols defined in assembly.

  6. folkertdev commented on Oct 31, 2025

    @folkertdev
    Contributor

    thus marks the naked function as private

    Does that mean that the linkage becomes Linkage::Internal?

    it can't for symbols defined in assembly

    Right. The docs on .private_extern are pretty sparse, but to me it seems at least not incorrect to add it to internal naked functions.

  7. bjorn3 commented on Oct 31, 2025

    @bjorn3
    Member

    Does that mean that the linkage becomes Linkage::Internal?

    Yes

    Right. The docs on .private_extern are pretty sparse, but to me it seems at least not incorrect to add it to internal naked functions.

    I think so

  8. alexcrichton commented on Jan 6, 2026

    @alexcrichton
    Member

    A bit smaller as a reproduction:

    #[no_mangle]
    pub extern "C" fn foo() {
        bar::bar();
    }
    
    mod bar {
        pub fn bar() {
            unsafe {
                baz();
            }
        }
    
        #[unsafe(naked)]
        pub unsafe extern "C" fn baz() {
            std::arch::naked_asm!("ret")
        }
    }

    yields:

    $ rustc foo.rs --crate-type cdylib -O -Ccodegen-units=2
    error: linking with `cc` failed: exit status: 1
      |
      = note:  "cc" "-Wl,-exported_symbols_list" "-Wl,/var/folders/70/cx6_8hz93jz7kj_km0_5c9_40000gn/T/rustcCdhCXE/list" "/var/folders/70/cx6_8hz93jz7kj_km0_5c9_40000gn/T/rustcCdhCXE/symbols.o" "<3 object files omitted>" "<sysroot>/lib/rustlib/aarch64-apple-darwin/lib/{libstd-*,libpanic_unwind-*,libobject-*,libmemchr-*,libaddr2line-*,libgimli-*,libcfg_if-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,liblibc-*,librustc_std_workspace_core-*,liballoc-*,libcore-*,libcompiler_builtins-*}.rlib" "-lSystem" "-lc" "-lm" "-arch" "arm64" "-mmacosx-version-min=11.0.0" "-o" "libfoo.dylib" "-Wl,-dead_strip" "-dynamiclib" "-nodefaultlibs"
      = note: some arguments are omitted. use `--verbose` to show all linker arguments
      = note: Undefined symbols for architecture arm64:
                "foo::bar::baz::hdee9697fdd075596", referenced from:
                    _foo in foo.foo.2a68415ea2c694de-cgu.1.rcgu.o
              ld: symbol(s) not found for architecture arm64
              clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
    
    error: aborting due to 1 previous error
    
  9. added a commit that references this issue on Jan 6, 2026
  10. added a commit that references this issue on Jan 6, 2026
  11. added a commit that references this issue on Jan 6, 2026
  12. added a commit that references this issue on Jan 6, 2026
  13. added a commit that references this issue on Jan 7, 2026
  14. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Jan 13, 2026
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-LTOArea: Link-time optimization (LTO)A-linkageArea: linking into static, shared libraries and binariesA-nakedArea: `#[naked]`, prologue and epilogue-free, functions, https://git.io/vAzzSC-bugCategory: This is a bug.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions