fix(restore): keep the executable bit on custom-harness test binaries - #648
Open
glebpom wants to merge 1 commit into
Open
Conversation
A `[[test]] harness = false` target supplies its own `main`, so cargo
invokes rustc with neither `--test` nor `--crate-type`. Its extensionless
output therefore classifies as `Other("rustc:unknown")`, whose link
strategy is `Hardlink`, and the restore path derived the output's
permissions from that strategy alone — so no `0o755` was applied. The
restored test binary comes back non-executable and cargo fails the run
with "Permission denied (os error 13)".
The resulting mode depends on which materialization path runs, and only
one of them preserves the bit by accident:
reflink cache+target on one CoW filesystem fresh file at the umask
hardlink one filesystem, copy-ingested blob blob's 0o555 — survives
hardlink one filesystem, reflink-ingested blob's 0o444
copy cache and target on different fs 0o644, hardcoded
So this is not specific to any filesystem: reflink-capable volumes (btrfs,
XFS with reflink=1, ZFS >= 2.2, bcachefs, APFS) hit it, and so does any
setup whose cache and build directories live on separate filesystems, ext4
included. The one configuration that works is a same-filesystem hardlink
from a copy-ingested blob, which is what a plain ext4 checkout produces —
and why this went unnoticed.
The executable bit recorded at insert time is the reliable signal, and the
insert side already trusts it over the filename (`hardlink_eligible`
refuses to hardlink anything carrying a mode bit). Restore now trusts it
the same way, which also keeps executables on the independent-inode path
so a post-build `strip` or codesign cannot reach back into the shared blob.
Covered by unit tests in `wrapper::tests`, which assert the contract
directly without depending on filesystem behaviour, and by a new
end-to-end `custom_harness_test` that builds the
`test-projects/custom-harness` fixture through the real wrapper, drops the
target dir, and lets cargo execute the restored binary. That test runs on
the repository's filesystem rather than TMPDIR, which would otherwise land
in the one blind spot above.
glebpom
force-pushed
the
fix/restore-executable-bit-for-custom-harness-tests
branch
from
July 31, 2026 17:07
f1a53d8 to
eda739c
Compare
glebpom
marked this pull request as ready for review
July 31, 2026 17:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A
[[test]] harness = falsetarget supplies its ownmain, so cargoinvokes rustc with neither
--testnor--crate-type. Its extensionlessoutput therefore classifies as
Other("rustc:unknown"), whose linkstrategy is
Hardlink, and the restore path derived the output'spermissions from that strategy alone — so no
0o755was applied.On a CoW filesystem the bit is lost twice over: the reflink ingest writes
the blob into a fresh temp file that
set_blob_readonlyleaves at0o444,and the reflink restore then creates the output at the process umask and
never chmods it. The restored test binary comes back non-executable and
cargo fails the run with "Permission denied (os error 13)". On tmpfs both
paths fall back to
fs::copy, which preserves the source mode and hidesthe whole thing.
The executable bit recorded at insert time is the reliable signal, and the
insert side already trusts it over the filename (
hardlink_eligiblerefuses to hardlink anything carrying a mode bit). Restore now trusts it
the same way, which also keeps executables on the independent-inode path
so a post-build
stripor codesign cannot reach back into the shared blob.Covered by unit tests in
wrapper::tests, which reproduce withoutdepending on filesystem capabilities, and by a new end-to-end
custom_harness_testthat builds thetest-projects/custom-harnessfixture through the real wrapper, drops the target dir, and lets cargo
execute the restored binary. The e2e test runs on the repository's
filesystem rather than TMPDIR, since tmpfs masks the failure.