Reproduction for rust-lang/cargo#12516,
plus one finding that is not yet on the issue: the stabilized
build.build-dir inherits the bug.
$ ./repro.sh
cargo: cargo 1.97.1 (c980f4866 2026-06-30)
control a: workspace a linked c::which() = a/c <-- workspace a's copy
b: workspace b linked c::which() = b/c <-- workspace b's copy
=> correct
target-dir a: workspace a linked c::which() = a/c <-- workspace a's copy
b: workspace b linked c::which() = a/c <-- workspace a's copy
=> *** WRONG: b linked a/c ***
build-dir a: workspace a linked c::which() = a/c <-- workspace a's copy
b: workspace b linked c::which() = a/c <-- workspace a's copy
=> *** WRONG: b linked a/c ***
Note there is no compile error. b builds successfully and prints a's
string. The original issue's repro surfaces as an unsafe fn type error, which
makes it look like a diagnostic problem; it isn't. The general case is a binary
silently linked against a different crate's code.
Two independent workspaces. Each has a path dependency named c, version
0.1.0, at the relative path c/. The two c crates have different contents.
That is the entire setup — there is nothing exotic here.
a/Cargo.toml b/Cargo.toml
a/src/main.rs b/src/main.rs
a/c/Cargo.toml b/c/Cargo.toml
a/c/src/lib.rs b/c/src/lib.rs <-- different contents
| toolchain | shared CARGO_TARGET_DIR |
shared build.build-dir only |
|---|---|---|
| cargo 1.97.1 (2026-06-30) | wrong | wrong |
| cargo 1.99.0-nightly (2026-07-29) | wrong | wrong |
./repro.sh # default toolchain
./repro.sh +nightly # or any rustup toolchain
Cargo gives every compilation unit an identity so artifacts can coexist in one
directory. That identity is the -C metadata hash: it becomes the -<hash>
suffix in libc-<hash>.rlib and the name of the fingerprint directory
.fingerprint/c-<hash>/.
The hash is derived from the package id, the profile, the feature set, the
rustc version and RUSTFLAGS. The package id contains a SourceId. For a
registry dependency the SourceId is the registry URL — globally unique. For a
path dependency it is the path, and cargo records that path relative to
the workspace root, not as an absolute path
(source_id.rs).
That is a deliberate design choice, not an oversight. Relative paths are what
make a build path-independent: the same workspace compiled at
/home/alice/proj and /build/ci/1234 produces identical artifacts. CI cache
restore across differently-named checkout directories depends on it, and so do
trim-paths and the eventual per-user artifact cache
(#5931).
The consequence is that a/c and b/c — different crates, different contents —
produce the same SourceId, so the same package id, so the same -C metadata
hash, so the same output filename and the same fingerprint directory.
In a shared directory:
- Build
a. Cargo compilesa/c, writeslibc-<H>.rliband.fingerprint/c-<H>/. - Build
b. Cargo computes the identity ofb/cand gets the same<H>. It finds.fingerprint/c-<H>/already present. - The freshness check compares the recorded dep-info against the files on
disk. Nothing in that check distinguishes
a/cfromb/c— the unit is declared fresh. - Cargo skips compiling
b/cand linksa/c's rlib intob.
Build order decides the outcome, which is the giveaway when this bites in the wild.
build.build-dir was stabilized in cargo 1.94 and splits intermediate
artifacts out of target/. A natural assumption is that sharing build-dir
while keeping target-dir per-workspace is a safe way to share the expensive
part of a build. It is not: deps/ and .fingerprint/ both live in build-dir,
so the collision follows build-dir exactly. The build-dir row above is
CARGO_BUILD_BUILD_DIR shared with a/target and b/target left separate.
This matters because the 2025h2 build-dir layout rework explicitly names sharing a build cache across workspaces as a motivation and proposes a user-wide cache built on this layout.
Anyone with several checkouts of the same workspace and a shared build
directory — most commonly git worktree with one worktree per branch, which
has become routine with coding agents. Every worktree presents the same
workspace-relative path for every workspace member, so the collision rate is
100%, not incidental.
The symptom is usually not this clean. It shows up as a compile error citing a
line in your own file for a definition that does not exist in your tree, while
git diff is clean — or as a test that passes against code you did not write.
Most people who hit it conclude they have a stale build and cargo clean it
away, which is why the issue has four reactions after three years.
Give each checkout its own target/ — cargo's default. Dependency artifacts
compile from global ~/.cargo paths and are identical across checkouts, so
they can be hardlinked between sibling checkouts (rsync -a --link-dest) for
zero bytes, which recovers most of what a shared directory would have bought.
A RUSTC_WRAPPER cannot fix this: cargo computes unit identity before it
invokes rustc, and expects specific filenames back.