Duplicate symbols in different link targets get combined
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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