Skip to content

libstore: skip optimisation when GC removes a link concurrently - #15904

Merged
xokdvium merged 1 commit into
NixOS:masterfrom
cachix:optimise-store-skip-gc-removed-links
May 26, 2026
Merged

libstore: skip optimisation when GC removes a link concurrently#15904
xokdvium merged 1 commit into
NixOS:masterfrom
cachix:optimise-store-skip-gc-removed-links

Conversation

@domenkozar

@domenkozar domenkozar commented May 22, 2026

Copy link
Copy Markdown
Member

Motivation

nix store optimise and auto-optimise-store can abort a build with:

filesystem error: cannot create hard link: No such file or directory [/nix/store/.links/1hyl5nszryd45bmwfln09f76hyan53zv40cvrymimcsp9qn6slhx] [/nix/store/.tmp-link-1196778-845337060]

https://github.com/cachix/devenv/actions/runs/26294238442/job/77408282471#step:6:2534

when a garbage collection runs at the same time.

The race

In optimisePath_, when a file with matching contents is already indexed, the flow is:

  1. confirm /nix/store/.links/<hash> exists,
  2. lstat it,
  3. compare inodes,
  4. create_hard_link(linkPath, tempLink) and then rename(tempLink, path).

The links-directory entry can have a link count of 1 (the original store path that created it was already collected, but removeUnusedLinks has not run yet). A concurrent GC's removeUnusedLinks then deletes that entry in the window between the existence check and create_hard_link. The source is gone, so create_hard_link fails with ENOENT, and the exception propagates and fails the whole operation.

This is the No such file or directory variant of #7273. The variant reported there is File exists, which was addressed by switching to makeTempPath in #14676; the ENOENT window described above is still unhandled.

Fix

Store optimisation is best effort, so a link disappearing underneath us is benign: skip optimising this path and let a later pass dedup it. This mirrors how the existing too_many_links and file_exists cases already shrug and return.

Two windows are guarded:

  • lstat(linkPath) becomes maybeLstat(linkPath), returning early if the entry vanished before the inode comparison.
  • the create_hard_link catch now returns on std::errc::no_such_file_or_directory.

Context

This is a race condition. There is no hook to deterministically remove linkPath mid call, so a non flaky regression test is not feasible, consistent with #7273 which also has none.

A concurrent garbage collection can remove an entry from the links
directory between the moment optimisePath_ checks for it and the moment
it creates the hard link. This produced "cannot create hard link: No
such file or directory" and failed the whole build.

Treat the vanished link as a benign race and skip optimising the path,
since a later pass will dedup it. Guard both the lstat (now maybeLstat)
and the create_hard_link call, mirroring the existing too_many_links and
file_exists handling.

Relates to NixOS#7273

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@domenkozar
domenkozar requested a review from Ericson2314 as a code owner May 22, 2026 19:36
@github-actions github-actions Bot added the store Issues and pull requests concerning the Nix store label May 22, 2026

@xokdvium xokdvium left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me.

@xokdvium
xokdvium added this pull request to the merge queue May 26, 2026
Merged via the queue into NixOS:master with commit a450f6f May 26, 2026
17 checks passed
kolmodin pushed a commit to kolmodin/nix that referenced this pull request May 30, 2026
…oved-links

libstore: skip optimisation when GC removes a link concurrently
@xokdvium xokdvium mentioned this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

store Issues and pull requests concerning the Nix store

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants