rust-lang / rust-lang/rust

Duplicate symbols in different link targets get combined

Open
#155,969 18 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-linkage C-bug F-raw_dylib needs-triage O-windows
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

When importing a symbol with the same name from multiple, differently named dynamic libraries, only the first symbol gets imported: the second, third, etc. symbols are combined and point to the first one. This can cause unexpected results.

Test case

The following test demonstrates the issue based on the RtlFillMemory symbol available on Windows in kernel32.dll and ntdll.dll. Even though it is imported from two different dynamic libraries in the code, it only ends up once in the compiled binary.

The code relies on the imported symbol not being forwarded, which would otherwise cause the test to return 0 (but the dumpbin output would still show the issue). On my Windows 11 24H2 installation, the RtlFillMemory symbols of kernel32.dll and ntdll.dll are not forwarded.

Compile the following code with panic = "abort" in release mode:

#![no_main]
#![no_std]
#![windows_subsystem = "console"]

#[cfg(not(test))]
#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

// Return an error code != 0, if the imported symbol address does not match the one determined at runtime (via `GetProcAddress`).
#[unsafe(no_mangle)]
extern "C" fn mainCRTStartup() -> u32 {
    unsafe {
        let k = GetProcAddress(GetModuleHandleA(c"kernel32.dll".as_ptr()), c"RtlFillMemory".as_ptr());
        let n = GetProcAddress(GetModuleHandleA(c"ntdll.dll".as_ptr()), c"RtlFillMemory".as_ptr());
        if k == 0 || n == 0 { return u32::MAX; } // Sanity check

        if k != kernel32::RtlFillMemory as *const () as usize {
            1
        } else if n != ntdll::RtlFillMemory as *const () as usize {
            2
        } else {
            0
        }
    }
}

// Duplicate symbol name in different dynamic libraries
mod kernel32 {
    #[link(name = "kernel32", kind = "raw-dylib")]
    unsafe extern "C" {
        pub(crate) fn RtlFillMemory();
    }
}

mod ntdll {
    #[link(name = "ntdll", kind = "raw-dylib")]
    unsafe extern "C" {
        pub(crate) fn RtlFillMemory();
    }
}

// Helper imports
#[cfg_attr(not(target_arch = "x86"), link(name = "kernel32", kind = "raw-dylib"))]
#[cfg_attr(target_arch = "x86", link(name = "kernel32", kind = "raw-dylib", import_name_type = "undecorated"))]
unsafe extern "system" {
    fn GetModuleHandleA(name: *const core::ffi::c_char) -> usize;
    fn GetProcAddress(handle: usize, name: *const core::ffi::c_char) -> usize;
}

Running this code via cargo run --release prints: error: process didn't exit successfully (exit code: 2).

dumpbin /imports output:

Imports:
    kernel32.dll
        GetProcAddress
        RtlFillMemory
        GetModuleHandleA

Switching the order of the kernel32 and ntdll modules in the code instead prints: ... (exit code: 1).

dumpbin /imports output:

Imports:
    ntdll.dll
        RtlFillMemory

    kernel32.dll
        GetModuleHandleA
        GetProcAddress

It is also apparent the symbols are combined into one when using the following mainCRTStartup code instead, as no imports end up in the compiled binary, they are completely optimized out, and the program returns 1 (true):

(kernel32::RtlFillMemory as *const () as usize == ntdll::RtlFillMemory as *const () as usize) as u32

dumpbin /imports output:

Imports:

Expected result

Ideally, symbols of different link targets should not be combined. Otherwise, at least a compiler warning would be useful.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the provided Windows test case using the two raw-dylib imports of RtlFillMemory and inspect how the compiler handles duplicate symbol names from different link targets. Done means the compiled binary preserves separate imports, or emits a compiler warning if they cannot be kept distinct.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.