docs/rationale/preload-linker-selection.md
The preload library must export exactly the interposer symbols that
src/c/shim.c defines (the exec family, posix_spawn*, popen,
pclose, system) plus LIBEXEC_VERSION, and hide everything else.
On ELF platforms this is done with a linker version script
(exports.map, generated by build.rs).
rustc injects its own version script into every cdylib link (an
anonymous tag exporting the crate's #[no_mangle] symbols, ending in
local: *). Two consequences, both verified experimentally (2026-07):
exports.map the interposers vanish from the dynamic table and
interception silently stops. This holds even when the archive is
linked with rustc-native -l static:+whole-archive=....lld and mold both accept the combination and export the union of both
scripts' globals. The union is also why the rust_<name>
implementation symbols currently appear in the dynamic symbol table
despite local: * in exports.map: rustc's script lists them as
global. This over-export is cosmetic (the names are prefixed, and
calls resolve within the library), but it is a consequence to know
about.
On x86_64-unknown-linux-gnu, current stable rustc links cdylibs
with its bundled rust-lld by default (it passes
-B <sysroot>/lib/rustlib/<host>/bin/gcc-ld and -fuse-ld=lld
itself). Targets without that default, such as aarch64, fall back to
the system cc default linker, GNU ld -- which is why issue #662
appeared on ARM64 only.
build.rs selects the linker for the ELF cdylib link with a fallback
chain instead of hard-coding -fuse-ld=lld:
<sysroot>/lib/rustlib/<host>/bin/gcc-ld/ld.lld exists (pass
-B for that directory plus -fuse-ld=lld);ld.lld (-fuse-ld=lld);mold (-fuse-ld=mold);lld or mold installed; both are widely
packaged. The failure without them is an early, explicit warning
followed by the linker error, instead of only the cryptic error.PATH only. A compiler-internal ld.lld
that cc would find through its own exec-prefix (but which is not
on PATH) falls through to the warning branch; probing
cc -print-prog-name=ld.lld could close that gap if it ever
matters in practice.rust_<name> union-export remains. Removing it would need a
function-pointer indirection between the shim and the Rust
implementation (so the Rust side has no #[no_mangle] symbols), or
a rustc-side mechanism to exclude them. Revisit if a strict export
surface becomes a requirement.Rejected alternatives:
cc from a staticlib. Linker-clean
(a single version script links fine under GNU ld, verified), but
stable cargo cannot orchestrate it: a build script cannot depend on
a sibling crate's staticlib artifact (the gap that RFC 3028
"artifact dependencies" exists to fill), a crate's own build script
runs before the crate compiles, and moving the link outside cargo
would break the single-command cargo build workflow and add a
packaging step. Revisit when cargo's artifact dependencies
stabilize.exports.map. GNU ld rejects any
combination that includes an anonymous tag, and rustc's tag is
anonymous; verified.exports.map in favor of rustc's own export handling.
Native-archive symbols are never in rustc's export list; verified.interception-preload-mechanism-fuse-ld=lld)