Repository navigation
rust: temporary and aside names are per process and created exclusively (#482) - #516
Merged
Merged
Conversation
…ly (#482) Rust named every temporary directory, temporary file and aside directory under the cache from a clock reading and the thread id. The main thread is ThreadId(1) in every process, so two processes that read the clock in the same step chose the same name. They then shared one temporary directory (create_dir_all reuses an existing one, fs::write truncates an existing file), the first to rename it into place took the other's half-written files with it, and the other failed on a path that had vanished: CHTYPES_SOURCE_UNREACHABLE "io error: No such file or directory". Every such name is now the process id and 64 random bits, and is created exclusively (create_dir, create_new): a name another process holds is never reused, another is drawn. The entry being replaced is moved into a fresh .stale-* directory, as in Go, Python and TypeScript. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…HANGELOG (#482) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Closed
7 tasks done
EricAndrechek
added a commit
that referenced
this pull request
Oct 7, 2026
…t for more The first CI run of the ABI v2 matrix took 6 to 14 s a round, so the coarse-clock leg gets the natural leg's three rounds (30 installs per binding). Under the 16% per-install failure rate #516 measured for the old Rust naming under a coarse clock, 30 clean installs leave a 0.5% chance of missing it, against 3% at two rounds. interop_rounds sets both counts for a deliberate heavier dispatch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Summary
A residual concurrent-install race in Rust (#482's follow-up). It ships in 1.1.0. One
v1-cache-interoprun at 08cfec9 failed 1 of 24 Rust installs withCHTYPES_SOURCE_UNREACHABLE: io error: No such file or directory (os error 2).Cause (measured on probe #514). Rust built every temporary name under the cache from
unique_suffix(), which was a clock reading plus the thread id. That covers the temporaryoci-layout,index.json, blob andverified.jsonfiles, the.tmp-*unpack directory, and the.stale-*aside. The main thread isThreadId(1)in every process, so two processes that read the clock in the same step drew the same name.create_dir_allreuses an existing directory andfs::writetruncates an existing file, so the two then shared one temporary path. The first rename won and the other failed withENOENT.The failing paths, from 1.1.0's path-naming messages, are:
<cache>/oci-layoutand<cache>/index.json(the writesensure_layoutmakes at startup), which account for nearly all of them;.tmp-*/verified.jsonand blobs.With a 32 MiB library and a forced collision, a process could also exit 0 naming an entry whose library was torn.
verifythen failed. The Linux runners' clock is ns-grained and darwin's is µs-grained, so the race is rare on Linux and more frequent on darwin or any coarse clock.Fix (Rust only).
unique_suffix()is now the process id plus 64 random bits. They come fromRandomState, whose SipHash keys are drawn from the OS random source in every process.create_dirfor directories andOpenOptions::create_newfor files, retrying onAlreadyExists. A name another process holds is never reused, even if it repeats..stale-*directory (<stale>/old), as Go, Python and TypeScript do. A failed rename removes its temporary file.The other bindings do not share the flaw, and this PR does not change them (read from the code, and measured 0 below):
rand.Uint64names and creates them exclusively (O_EXCL,Mkdir).os.urandomwithO_EXCL, andtempfile.mkdtemp.randomBytes(6), and.stale-*comes frommkdtemp.In all four, a vanished path in the scan or select path is already read as "absent": record reads, listings and the fault probe (read).
docs/guides/fetch-v1.md§1 gains one sentence stating the rule for every binding.Measurements (probe #514, hosted runners, 8 processes released together, one fresh cache per round)
The fix head is f6440ff for the Linux natural and planted legs, and e661922 for the coarse-clock and darwin legs. The Rust source is identical in both; the commits after f6440ff change only docs and the CHANGELOG.
Natural, Linux (ubuntu-latest; clock
tsc, ns-grained), small layer:The failures were:
SOURCE_UNREACHABLE"io error: No such file or directory (os error 2)".CACHE_UNUSABLE"<cache>/oci-layoutis unusable as a cache: unwritable (ENOENT)" ×28, and the same for<cache>/index.json×3.Natural, Linux, 32 MiB library:
Coarse clock (100 µs), Linux, Rust:
The shim is an
LD_PRELOADthat roundsCLOCK_REALTIMEdown to 100 µs. Each leg first checks that it takes effect. The 1.1.0 row validates it: 192 under the shim against 212 with the rounding patched into the naming.Darwin (macos-latest, arm64, 3 CPUs;
CLOCK_REALTIMEis whole microseconds, 300000 of 300000 readings), small layer:index.jsonunwritable ENOENT)DYLD_INSERT_LIBRARIESshim), rust1.0.4 was not run on darwin.
Planted collision, Linux, Rust (every other name draw repeats across the eight processes):
The messages were:
SOURCE_UNREACHABLE"io error: No such file or directory";ARTIFACT_CORRUPT"library<cache>/unpacked/sha256/.tmp-…/libchtypes.so: No such file or directory";ARTIFACT_CORRUPTdigest mismatch.CACHE_UNUSABLEunwritable (ENOENT) atoci-layoutandindex.json, plus the sameARTIFACT_CORRUPTpair.Test plan
rust/src/ocifetch/layout.rs. With only a test seam added, they FAIL on 1.1.0'slayout.rs(4 failed, 7 passed) and pass on this head (11 passed), measured in the probe'sregression-testsjobs:policy-merge-check.py --selftestand--check-ci-names,lint-public,lint-spelling,lint-cited-paths, markdownlint-cli2 0.18.1, dprint 0.57.4 check, andrustfmt --checkonlayout.rs: all clean locally.security(report only), which fails onmainat d15083f too. The three skipped checks run on dispatch only.Related issues
Refs #482. Folded into 1.1.0 (not yet tagged). CHANGELOG bullet under
[1.1.0]/ Fixed in Rust only, since no other binding changed.🤖 Generated with Claude Code