Skip to content

fix(daemon): verify PID identity so a recycled PID can't wedge startup - #551

Merged
githubrobbi merged 1 commit into
mainfrom
fix/daemon-pid-identity-liveness
Jul 12, 2026
Merged

fix(daemon): verify PID identity so a recycled PID can't wedge startup#551
githubrobbi merged 1 commit into
mainfrom
fix/daemon-pid-identity-liveness

Conversation

@githubrobbi

Copy link
Copy Markdown
Collaborator

Summary

The daemon singleton guard gated on a bare liveness probe (is_process_alive(pid)OpenProcess/kill(0)). On Windows that is unsound: PIDs are recycled aggressively and a terminated daemon can linger as a zombie. A stale daemon.pid (left when a foreground uffsd is Ctrl-C'd without cleanup) then resolves to a live but unrelated process, so the guard concludes "another daemon is running" and refuses to start — forever, because the stale-file cleanup only ran on the dead-PID branch. Every uffsd / uffs --daemon start then failed with "Another daemon instance is already running" / "did not become ready in time" until the user manually deleted daemon.pid.

Root cause: the liveness check confirmed a process existed, never that it was uffsd. The client already got this right; the daemon had not caught up.

What changed

  • Identity over liveness. is_daemon_process_alive resolves the live process's image path (QueryFullProcessImageNameW on Windows, proc_pidpath / /proc on unix) and only honors the singleton guard when the image is a uffsd binary. A recycled/foreign PID is treated as a stale file and reclaimed; an unreadable image errs toward "running" so two daemons never race to bind the pipe.
  • Testable decomposition. The decision is a pure, exhaustively unit-tested helper (pid_entry_is_running_daemon) plus a name predicate (is_uffsd_image); check_stale_pid_at is extracted so the whole flow runs against a tempfile.
  • Honest uffs --status. An unreachable-but-present PID file is no longer always labelled "stale" — a live daemon that is not answering on IPC (still loading, or wedged) is reported distinctly.

Testing

Recycled-PID reclaim, dead/zero/unparseable PID reclaim, missing file, full identity-decision branch matrix, and daemon-name matching (incl. a Windows-only backslash-path case). Validated locally with lint-prod, lint-prod-windows, lint-tests, lint-tests-windows, rustdoc, and fmt.

`check_stale_pid` gated the singleton guard on `is_process_alive(pid)`, a
bare `OpenProcess`/`kill(0)` existence probe. On Windows that is unsound:
PIDs are recycled aggressively, and a terminated daemon can linger as a
zombie while a handle to it stays open. A stale PID file (left when a
foreground `uffsd` is Ctrl-C'd without cleanup) then resolves to a live
but unrelated process, so the guard concludes "another daemon is running"
and refuses to start — forever, because the stale-file cleanup only runs
on the dead-PID branch. Every `uffsd`/`uffs --daemon start` then fails
with "Another daemon instance is already running" / "did not become ready
in time" until the user manually deletes daemon.pid.

Root cause: the liveness check confirmed *a* process existed, never that
it was uffsd. The client already got this right (`is_pid_alive` and the
connect-time identity check both confirm the image is uffsd); the daemon
had not caught up.

Fix: layer an identity check over the liveness probe. `is_daemon_process_alive`
resolves the live process's image path (QueryFullProcessImageNameW on
Windows, proc_pidpath/`/proc` on unix) and only honors the singleton guard
when the image is a uffsd binary. A recycled/foreign PID is treated as a
stale file and reclaimed; an unreadable image errs toward "running" so two
daemons never race to bind the pipe. The decision is split into a pure,
exhaustively unit-tested helper (`pid_entry_is_running_daemon`) plus a
name predicate (`is_uffsd_image`); `check_stale_pid_at` is extracted so the
whole flow is testable against a tempfile.

Also make `uffs --status` honest: instead of always labelling an
unreachable-but-present PID file "stale", report a live daemon that is not
answering on IPC (still loading, or wedged) distinctly, so a wedged daemon
no longer masquerades as "not running".

Tests: recycled-PID reclaim, dead/zero/unparseable PID reclaim, missing
file, full identity-decision branch matrix, and daemon-name matching
(incl. a Windows-only backslash-path case). Validated with lint-prod,
lint-prod-windows, lint-tests, lint-tests-windows, rustdoc, and fmt.
@githubrobbi
githubrobbi enabled auto-merge July 12, 2026 11:39
@githubrobbi
githubrobbi added this pull request to the merge queue Jul 12, 2026
Merged via the queue into main with commit 8e310af Jul 12, 2026
21 checks passed
@githubrobbi
githubrobbi deleted the fix/daemon-pid-identity-liveness branch July 12, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant