The rust-governor shim's build lock can be captured permanently by the sccache daemon, after which every cargo invocation on the machine blocks forever with no timeout and no diagnostic.
Mechanism
~/.local/bin/cargo serialises builds on a per-user flock (shim lines 107-110):
exec 9>"$LOCK"
if ! flock -n 9; then
notify_wait
flock 9
fi
The comment above it (line 104) states: "The fd survives exec, so the lock lives exactly as long as the build and releases on any exit, crash included."
That holds only if no long-lived descendant inherits fd 9. A flock belongs to the open file description, so any daemon started during a governed build keeps the lock alive after the build itself exits.
sccache is exactly such a daemon. When hyperi-sccache.service is not running, cargo auto-spawns sccache inside the locked scope; it inherits fd 9, daemonises, and holds the lock until it is killed.
Trigger
hyperi-sccache.service ships disabled (with preset: enabled), so on a fresh SOE machine the first Rust build spawns sccache ad-hoc and the lock is captured immediately. The shim already anticipates the service (line 8: "sccache-hosted compiles are captured separately: hyperi-sccache.service carries Slice=rust-build.slice") - but nothing enables or starts it.
Symptom
A build that looks alive but does nothing:
- 0% CPU, no
rustc children
wchan is do_wait; its only child is flock 9
/proc/locks shows a holder PID that no longer exists
Observed while running a local quality gate over a Rust workspace:
$ stat -c '%i' /run/user/1001/hyperi-rust-govern-1001.lock
107
$ rg ":107 " /proc/locks
7: FLOCK ADVISORY WRITE 3078836 00:3f:107 0 EOF
7: -> FLOCK ADVISORY WRITE 3078951 00:3f:107 0 EOF
$ ps -p 3078836 # holder is dead
$ ls -l /proc/3078858/fd/9
... -> /run/user/1001/hyperi-rust-govern-1001.lock # sccache holds it
Killing sccache releases the lock, but the respawned daemon re-inherits fd 9 within seconds, so the wedge returns on the next cold build.
Impact
hyperi-ci check --quick over the same Rust workspace, same machine, same commit:
|
elapsed |
avg CPU |
| lock captured |
2670 s (44.5 min) |
1% |
| after fix |
32.75 s |
95% |
An 81x difference that presents as "Rust builds are slow" or "the CI hook is unusable", with nothing in any log to indicate a lock is involved. It affects every Rust build on the box, not just hyperi-ci. In this case it was misattributed to disk pressure for some time.
The macOS path does not have this bug
The non-flock branch (lines 113-130) stamps the holder's PID into the lock directory and clears it when kill -0 fails, so staleness is detected there. The Linux flock path has no equivalent, precisely because it assumes fd lifetime equals process lifetime.
Suggested fix
-
Enable and start hyperi-sccache.service in the role. It is already written and already carries Slice=rust-build.slice; it just is not enabled. This removes the trigger.
-
Bound the wait, as defence in depth. Any long-lived daemon spawned during a governed build reproduces this, so removing sccache as the trigger does not remove the class. flock -w <timeout> 9 degrades a captured lock into a bounded delay plus a warning naming the holder, instead of a permanent hang. A build that runs unserialised is better than one that never runs.
Workaround
pkill -x sccache
systemctl --user enable --now hyperi-sccache.service
Then confirm the service's fd 9 is a socket rather than the lock file, and that /proc/locks no longer lists the lock inode.
Not verified
- Whether daemons other than sccache (rust-analyzer, a test harness leaving a background process) trigger the same capture. The mechanism says they would; only sccache was observed.
- Whether the role intends the service to be enabled and this is a deployment regression, or whether it was always opt-in.
The rust-governor shim's build lock can be captured permanently by the sccache daemon, after which every
cargoinvocation on the machine blocks forever with no timeout and no diagnostic.Mechanism
~/.local/bin/cargoserialises builds on a per-user flock (shim lines 107-110):The comment above it (line 104) states: "The fd survives exec, so the lock lives exactly as long as the build and releases on any exit, crash included."
That holds only if no long-lived descendant inherits fd 9. A flock belongs to the open file description, so any daemon started during a governed build keeps the lock alive after the build itself exits.
sccacheis exactly such a daemon. Whenhyperi-sccache.serviceis not running, cargo auto-spawns sccache inside the locked scope; it inherits fd 9, daemonises, and holds the lock until it is killed.Trigger
hyperi-sccache.serviceshipsdisabled(withpreset: enabled), so on a fresh SOE machine the first Rust build spawns sccache ad-hoc and the lock is captured immediately. The shim already anticipates the service (line 8: "sccache-hosted compiles are captured separately: hyperi-sccache.service carries Slice=rust-build.slice") - but nothing enables or starts it.Symptom
A build that looks alive but does nothing:
rustcchildrenwchanisdo_wait; its only child isflock 9/proc/locksshows a holder PID that no longer existsObserved while running a local quality gate over a Rust workspace:
Killing sccache releases the lock, but the respawned daemon re-inherits fd 9 within seconds, so the wedge returns on the next cold build.
Impact
hyperi-ci check --quickover the same Rust workspace, same machine, same commit:An 81x difference that presents as "Rust builds are slow" or "the CI hook is unusable", with nothing in any log to indicate a lock is involved. It affects every Rust build on the box, not just hyperi-ci. In this case it was misattributed to disk pressure for some time.
The macOS path does not have this bug
The non-flock branch (lines 113-130) stamps the holder's PID into the lock directory and clears it when
kill -0fails, so staleness is detected there. The Linux flock path has no equivalent, precisely because it assumes fd lifetime equals process lifetime.Suggested fix
Enable and start
hyperi-sccache.servicein the role. It is already written and already carriesSlice=rust-build.slice; it just is not enabled. This removes the trigger.Bound the wait, as defence in depth. Any long-lived daemon spawned during a governed build reproduces this, so removing sccache as the trigger does not remove the class.
flock -w <timeout> 9degrades a captured lock into a bounded delay plus a warning naming the holder, instead of a permanent hang. A build that runs unserialised is better than one that never runs.Workaround
pkill -x sccache systemctl --user enable --now hyperi-sccache.serviceThen confirm the service's fd 9 is a socket rather than the lock file, and that
/proc/locksno longer lists the lock inode.Not verified