builders: backport nix GC AddTempRoot race fix
The builders run Nix 2.34.7, whose daemon ignores an AddTempRoot registered while a GC is already in its deletion phase (the deletion loop never re-checks tempRoots). hydra-builder pins each build's inputs and outputs with per-build AddTempRoot calls, so the hourly GC races those pins and collects paths mid-build, failing with "path '...' is not valid" (NixOS/hydra#1806). Backport NixOS/nix#15992, which re-checks tempRoots before each deletion. Applied via appendPatches since pkgs.nix is the modular combined package and patches must reach the shared component source. Drop once the builders' nix includes the fix.
Jörg Thalheim committed
Jun 26, 2026 at 09:33 UTC
d6ab6014e328c6c083414d09b7c84f3a8733897e
3 files changed
+48
-1
builders/common/nix-gc-addtemproot-race.patch
new
+37
@@ -0,0 +1,37 @@
1
+diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc
2
+index 16b81abf2..d3c46ae8d 100644
3
+--- a/src/libstore/gc.cc
4
++++ b/src/libstore/gc.cc
5
+@@ -680,6 +680,32 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
6
+ if (!dead.insert(path).second)
7
+ continue;
8
+ if (shouldDelete) {
9
++ /* Re-check tempRoots before deleting and set pending
10
++ to synchronise with addTempRoot. Between the BFS
11
++ and this deletion loop, new temproots may have been
12
++ added via the GC socket by a concurrent process
13
++ (e.g. an evaluator calling addTempRoot). The BFS
14
++ only checks tempRoots when it first visits a path,
15
++ but the "pending" mechanism only blocks the socket
16
++ handler for the single path currently being visited,
17
++ not for paths already queued for deletion. */
18
++ {
19
++ auto hashPart = std::string(path.hashPart());
20
++ auto shared(_shared.lock());
21
++ if (shared->tempRoots.count(hashPart)) {
22
++ debug(
23
++ "not deleting '%s' because it became a temporary root after initial scan",
24
++ printStorePath(path));
25
++ alive.insert(path);
26
++ continue;
27
++ }
28
++ shared->pending = hashPart;
29
++ }
30
++ Finally resetPending([&]() {
31
++ auto shared(_shared.lock());
32
++ shared->pending.reset();
33
++ wakeup.notify_all();
34
++ });
35
+ try {
36
+ invalidatePathChecked(path);
37
+ deleteFromStore(path.to_string(), true);
builders/common/nix.nix
+5
-1
@@ -7,7 +7,11 @@
7
8
{
9
nix = {
10
- package = pkgs.nix;
10
+ # Backport of NixOS/nix#15992: a temp root registered while a GC is in its
11
+ # deletion phase was ignored, so hydra-builder's per-build AddTempRoot pins
12
+ # raced the hourly GC and inputs/outputs were collected mid-build
13
+ # (NixOS/hydra#1806).
14
+ package = pkgs.nix.appendPatches [ ./nix-gc-addtemproot-race.patch ];
15
nrBuildUsers = config.nix.settings.max-jobs + 32;
16
17
gc =
macs/common/nix.nix
+6
@@ -1,5 +1,6 @@
1
{
2
config,
3
+ pkgs,
4
...
5
}:
6
@@ -9,6 +10,11 @@
10
];
11
12
nix = {
13
+ # Backport of NixOS/nix#15992: a temp root registered while a GC is in its
14
+ # deletion phase was ignored, so per-build AddTempRoot pins raced the
15
+ # periodic GC and inputs/outputs were collected mid-build (NixOS/hydra#1806).
16
+ package = pkgs.nix.appendPatches [ ../../builders/common/nix-gc-addtemproot-race.patch ];
17
+
18
settings = {
19
extra-experimental-features = [
20
"nix-command"