A hardlink clone shares artifact inodes with the cache it was cloned from, on the premise — stated at unshare_mutable_paths in scripts/cache-lib.sh — that "rustc and the linker replace deps/*.rlib, *.rmeta, and binaries rather than truncating them in place".
That is true of rustc's own outputs and false of the linker's. rustc writes an .rlib/.rmeta to a temporary and renames it over the path. A linked executable is written through whatever inode is already at the path, so a build inside a cp -al clone rewrites the source's copy.
Measured
One bin crate with a unit test, cargo test --no-run, warm target dir, cp -al clone, change the source, build in the clone, compare the source. Measured 2026-08-27 on daniel-desktop, Linux/ext4:
In every case the clone's file and the source's file still share one inode afterwards, and both hold the clone's content. Both build-dir layouts, stable and nightly alike — this is not a layout-v2 problem, and deps/ is shared today.
Consequence
The same class of failure the hardlink scheme exists to prevent, one artifact family over. Branch B clones base's cache and builds; base's cached test binary now holds B's code. Base's fingerprint files are privately owned and therefore intact, so base's next build compares them against its own unchanged sources, reports Fresh, does not relink, and runs B's binary. Silent wrong-artifact reuse — and unlike the dep-info case it is the executable that actually runs.
Latent in practice only to the extent that a consuming repo's cached test binaries get reused across branches, which is precisely what the cache is for.
The trigger is NOT fully characterised
Cargo re-creates the path before linking when it also has to uplift the result — a bin target's deps/<bin>-<hash> has a hardlink twin at <profile>/<bin> — and those cases were measured safe: the clone's link produced a fresh inode with different content while the source's copy survived.
Every executable measured intact was of that shape, and every one measured rewritten had no twin. Whether the twin is the mechanism or merely a correlate of it was not determined.
Correction, 2026-08-27. An earlier revision of this paragraph claimed hardlink count at link time was ruled out as the discriminator, on the strength of a lib+bin crate whose two test binaries were both nlink == 1 and appeared to behave differently. Re-checked on review: the "intact" one had not been rebuilt at all — same content, same inode, the unit was never exercised — so it demonstrated nothing, and forcing both to rebuild rewrote both. That claim asserted more than the measurement showed and is withdrawn. Output size growth and the linker in use (mold via the machine's global ~/.cargo/config.toml, and the default linker) were genuinely ruled out.
So the safe cases cannot presently be enumerated. That is the stated limitation of this ticket, not a footnote: any fix that shares some linker outputs needs a rule that can tell a rustc-renamed artifact from a linker-written one at selection time, and no such rule has been demonstrated.
Fix as shipped
gitdan-actions#16 treats every executable regular file as mutable, under both layouts, because the executable bit is the only property that reliably separates linker output from rustc output. .rlib, .rmeta and incremental/ stay shared and are the bytes worth sharing.
That is deliberately broad, and it costs. On a real 5.5 GB Bevy target directory the real-copied share goes 8.98% → 14.08% whole-tree, and 36.4% → 57.0% excluding incremental/ — the latter being the CI-shaped figure, since CI sets CARGO_INCREMENTAL=0. The copy is unconditional per clone (a fresh cp -al leaves every file with nlink >= 2, so _unshare_files' -links +1 filter cannot skip anything) and a clone happens twice per job, on seed and on publish. It does not amortise across runs.
What a narrower fix would need
A selection-time discriminator between "rustc renamed this into place" and "the linker wrote this". The obvious candidate — is_executable AND has no in-tree hardlink twin — is consistent with every measurement here, and it is not worth implementing: measured on a real 5.5 GB tree it would unshare exactly the same 283 MB, because the 489 MB of twinned executables are build-script binaries under build/ that the old selection already real-copied in full. Exempting them would reduce the status-quo cost on files about whose safety there is no evidence, not this rule's cost.
So the prize is not there. A rule that recovers real bytes would have to share some untwinned executable, which is precisely the set every rewrite was measured in — it needs the mechanism established first, not another filesystem heuristic. Reading Cargo's own unit metadata rather than inferring from the filesystem is the other direction worth trying. Correctness is not negotiable against either.
A hardlink clone shares artifact inodes with the cache it was cloned from, on the premise — stated at `unshare_mutable_paths` in `scripts/cache-lib.sh` — that "rustc and the linker replace `deps/*.rlib`, `*.rmeta`, and binaries rather than truncating them in place".
**That is true of rustc's own outputs and false of the linker's.** rustc writes an `.rlib`/`.rmeta` to a temporary and renames it over the path. A linked executable is written through whatever inode is already at the path, so a build inside a `cp -al` clone rewrites the *source's* copy.
## Measured
One bin crate with a unit test, `cargo test --no-run`, warm target dir, `cp -al` clone, change the source, build in the clone, compare the source. Measured 2026-08-27 on daniel-desktop, Linux/ext4:
| toolchain | cargo | layout | file | source after |
|---|---|---|---|---|
| stable | 1.93.1 (083ac5135 2025-12-15) | v1 | `debug/deps/bprobe-0ecbea0cebf59df5` | **rewritten** |
| nightly | 1.96.0-nightly (f298b8c82 2026-02-24) | v1 | `debug/deps/bprobe-6a091d813b2be60d` | **rewritten** |
| nightly-2026-07-02 | 1.98.0-nightly (a335d47ff 2026-06-26) | v1 | `debug/deps/bprobe-6a091d813b2be60d` | **rewritten** |
| nightly-2026-08-25 | 1.100.0-nightly (e8cb624d5 2026-08-22) | v2 | `debug/build/bprobe/6a091d813b2be60d/out/bprobe-6a091d813b2be60d` | **rewritten** |
In every case the clone's file and the source's file still share one inode afterwards, and both hold the clone's content. Both build-dir layouts, stable and nightly alike — **this is not a layout-v2 problem**, and `deps/` is shared today.
## Consequence
The same class of failure the hardlink scheme exists to prevent, one artifact family over. Branch B clones base's cache and builds; base's cached test binary now holds B's code. Base's fingerprint files are privately owned and therefore *intact*, so base's next build compares them against its own unchanged sources, reports `Fresh`, does not relink, and runs B's binary. Silent wrong-artifact reuse — and unlike the dep-info case it is the executable that actually runs.
Latent in practice only to the extent that a consuming repo's cached test binaries get reused across branches, which is precisely what the cache is for.
## The trigger is NOT fully characterised
Cargo re-creates the path before linking when it also has to uplift the result — a bin target's `deps/<bin>-<hash>` has a hardlink twin at `<profile>/<bin>` — and those cases were measured *safe*: the clone's link produced a fresh inode with different content while the source's copy survived.
**Every executable measured intact was of that shape, and every one measured rewritten had no twin.** Whether the twin is the mechanism or merely a correlate of it was not determined.
**Correction, 2026-08-27.** An earlier revision of this paragraph claimed hardlink count at link time was *ruled out* as the discriminator, on the strength of a lib+bin crate whose two test binaries were both `nlink == 1` and appeared to behave differently. Re-checked on review: the "intact" one **had not been rebuilt at all** — same content, same inode, the unit was never exercised — so it demonstrated nothing, and forcing both to rebuild rewrote both. That claim asserted more than the measurement showed and is withdrawn. Output size growth and the linker in use (mold via the machine's global `~/.cargo/config.toml`, and the default linker) *were* genuinely ruled out.
**So the safe cases cannot presently be enumerated.** That is the stated limitation of this ticket, not a footnote: any fix that shares *some* linker outputs needs a rule that can tell a rustc-renamed artifact from a linker-written one at selection time, and no such rule has been demonstrated.
## Fix as shipped
gitdan-actions#16 treats every executable regular file as mutable, under both layouts, because the executable bit is the only property that reliably separates linker output from rustc output. `.rlib`, `.rmeta` and `incremental/` stay shared and are the bytes worth sharing.
That is deliberately broad, and it costs. On a real 5.5 GB Bevy target directory the real-copied share goes 8.98% → 14.08% whole-tree, and **36.4% → 57.0%** excluding `incremental/` — the latter being the CI-shaped figure, since CI sets `CARGO_INCREMENTAL=0`. The copy is unconditional per clone (a fresh `cp -al` leaves every file with `nlink >= 2`, so `_unshare_files`' `-links +1` filter cannot skip anything) and a clone happens twice per job, on seed and on publish. It does not amortise across runs.
## What a narrower fix would need
A selection-time discriminator between "rustc renamed this into place" and "the linker wrote this". The obvious candidate — `is_executable AND has no in-tree hardlink twin` — is consistent with every measurement here, **and it is not worth implementing**: measured on a real 5.5 GB tree it would unshare exactly the same 283 MB, because the 489 MB of twinned executables are build-script binaries under `build/` that the *old* selection already real-copied in full. Exempting them would reduce the status-quo cost on files about whose safety there is no evidence, not this rule's cost.
So the prize is not there. A rule that recovers real bytes would have to share some *untwinned* executable, which is precisely the set every rewrite was measured in — it needs the mechanism established first, not another filesystem heuristic. Reading Cargo's own unit metadata rather than inferring from the filesystem is the other direction worth trying. Correctness is not negotiable against either.
claude
added the bug label 2026-08-27 19:16:07 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A hardlink clone shares artifact inodes with the cache it was cloned from, on the premise — stated at
unshare_mutable_pathsinscripts/cache-lib.sh— that "rustc and the linker replacedeps/*.rlib,*.rmeta, and binaries rather than truncating them in place".That is true of rustc's own outputs and false of the linker's. rustc writes an
.rlib/.rmetato a temporary and renames it over the path. A linked executable is written through whatever inode is already at the path, so a build inside acp -alclone rewrites the source's copy.Measured
One bin crate with a unit test,
cargo test --no-run, warm target dir,cp -alclone, change the source, build in the clone, compare the source. Measured 2026-08-27 on daniel-desktop, Linux/ext4:debug/deps/bprobe-0ecbea0cebf59df5debug/deps/bprobe-6a091d813b2be60ddebug/deps/bprobe-6a091d813b2be60ddebug/build/bprobe/6a091d813b2be60d/out/bprobe-6a091d813b2be60dIn every case the clone's file and the source's file still share one inode afterwards, and both hold the clone's content. Both build-dir layouts, stable and nightly alike — this is not a layout-v2 problem, and
deps/is shared today.Consequence
The same class of failure the hardlink scheme exists to prevent, one artifact family over. Branch B clones base's cache and builds; base's cached test binary now holds B's code. Base's fingerprint files are privately owned and therefore intact, so base's next build compares them against its own unchanged sources, reports
Fresh, does not relink, and runs B's binary. Silent wrong-artifact reuse — and unlike the dep-info case it is the executable that actually runs.Latent in practice only to the extent that a consuming repo's cached test binaries get reused across branches, which is precisely what the cache is for.
The trigger is NOT fully characterised
Cargo re-creates the path before linking when it also has to uplift the result — a bin target's
deps/<bin>-<hash>has a hardlink twin at<profile>/<bin>— and those cases were measured safe: the clone's link produced a fresh inode with different content while the source's copy survived.Every executable measured intact was of that shape, and every one measured rewritten had no twin. Whether the twin is the mechanism or merely a correlate of it was not determined.
Correction, 2026-08-27. An earlier revision of this paragraph claimed hardlink count at link time was ruled out as the discriminator, on the strength of a lib+bin crate whose two test binaries were both
nlink == 1and appeared to behave differently. Re-checked on review: the "intact" one had not been rebuilt at all — same content, same inode, the unit was never exercised — so it demonstrated nothing, and forcing both to rebuild rewrote both. That claim asserted more than the measurement showed and is withdrawn. Output size growth and the linker in use (mold via the machine's global~/.cargo/config.toml, and the default linker) were genuinely ruled out.So the safe cases cannot presently be enumerated. That is the stated limitation of this ticket, not a footnote: any fix that shares some linker outputs needs a rule that can tell a rustc-renamed artifact from a linker-written one at selection time, and no such rule has been demonstrated.
Fix as shipped
gitdan-actions#16 treats every executable regular file as mutable, under both layouts, because the executable bit is the only property that reliably separates linker output from rustc output.
.rlib,.rmetaandincremental/stay shared and are the bytes worth sharing.That is deliberately broad, and it costs. On a real 5.5 GB Bevy target directory the real-copied share goes 8.98% → 14.08% whole-tree, and 36.4% → 57.0% excluding
incremental/— the latter being the CI-shaped figure, since CI setsCARGO_INCREMENTAL=0. The copy is unconditional per clone (a freshcp -alleaves every file withnlink >= 2, so_unshare_files'-links +1filter cannot skip anything) and a clone happens twice per job, on seed and on publish. It does not amortise across runs.What a narrower fix would need
A selection-time discriminator between "rustc renamed this into place" and "the linker wrote this". The obvious candidate —
is_executable AND has no in-tree hardlink twin— is consistent with every measurement here, and it is not worth implementing: measured on a real 5.5 GB tree it would unshare exactly the same 283 MB, because the 489 MB of twinned executables are build-script binaries underbuild/that the old selection already real-copied in full. Exempting them would reduce the status-quo cost on files about whose safety there is no evidence, not this rule's cost.So the prize is not there. A rule that recovers real bytes would have to share some untwinned executable, which is precisely the set every rewrite was measured in — it needs the mechanism established first, not another filesystem heuristic. Reading Cargo's own unit metadata rather than inferring from the filesystem is the other direction worth trying. Correctness is not negotiable against either.
claude referenced this issue2026-08-27 19:16:58 +00:00
claude referenced this issue2026-08-27 19:40:10 +00:00