Repository navigation
fix(core): don't mistake a stale daemon PID file for a running daemon (V2-1655) - #235
Nic-dorman wants to merge 7 commits into
Conversation
… (V2-1655) A daemon that dies without cleaning up (a reboot, an OOM kill, SIGKILL) leaves daemon.pid and daemon.port behind. Once the PID is reused by an unrelated process, `daemon info` and `daemon start` saw a live PID and treated it as the daemon: `info` reported running while `status` (which asks the daemon over HTTP) said not running, and `start` refused to start a new daemon, so the hint `status` prints led nowhere. `info` and `start` now classify the PID file through one helper that also requires the process to be identifiable as the daemon, the check `stop` already applied before signalling it. `start` removes stale files before spawning; otherwise its port poll would read the dead daemon's port straight back. macOS now checks the executable name via proc_pidpath instead of trusting any live PID. The `info` help text and README no longer claim its output is always JSON: it has been human-readable unless --json is passed since 920f872, which is the convention for every command. Adds unit tests for the reused-PID case and an e2e skill step (5.5). Closes V2-1655 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rustdoc's private_intra_doc_links lint fails the Documentation job under -D warnings. Describe the check inline instead of linking to `recorded_daemon`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c714134 to
da95adb
Compare
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — 2/2 seats reviewed (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3). The seats disagree; I confirmed the platform-identity gap below against the frozen head. CI checks were all passing at review time.
- P2 —
ant-core/src/node/daemon/client.rs:496-516(also Windows:519-545): the macOS and Windows validators accept any live process whose executable basename isant/ant.exe; unlike the Linux validator, they do not verify daemon-specific arguments. A stale PID reused by anotherantCLI process therefore classifies asLive, soinfocan again reportrunning: trueandstartcan refuse recovery. Please verify that the process is running the daemon command (or use another daemon-specific identity signal) on these platforms, and add a regression test using a same-executable, non-daemon process. The current test only uses the test harness executable, so it does not cover this case.
Coverage: 2/2 seats reviewed. sol-1 returned CHANGES_REQUIRED; glm-1 returned APPROVE but noted the executable-name limitation. The process-identity issue is reproducible from the platform checks in the diff; it blocks approval.
…d Windows too Review of #235: the macOS and Windows checks accepted any live process whose executable is named `ant`. A stale PID reused by another `ant` command, such as a long `ant file upload`, was still reported as the running daemon, and `start` still refused to start. Each platform now requires the process to be `ant` running `daemon run`, the arguments `daemon start` passes to the detached child. macOS and Windows read the arguments through `sysinfo`, which ant-core already uses for node adoption. The executable-name checks stay as they were: they work for another user's process and for an elevated one. Where the arguments can't be read, `sysinfo` reports none, and the name check alone decides. Linux keeps its /proc reading but uses the same rule, which is stricter than the previous "any `daemon` argument". New tests copy the test binary as `ant` and run it as `node daemon run` (accepted), as `file upload` (rejected), and as `node daemon status` behind a reused PID (`info` reports it not running). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — 2/2 seats reviewed (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3). CI checks pass on the reviewed head. sol-1 returned CHANGES_REQUIRED; glm-1 returned UNCERTAIN. I verified the findings below against the frozen source.
-
P2 —
ant-core/src/node/daemon/client.rs:498-500:runs_the_daemonaccepts any adjacentdaemon runpair anywhere in argv, not specifically the CLI command path. For example, anant file uploadinvocation with argumentsdaemon runpasses this check, so a reused PID can still be classified as the daemon andstartcan refuse recovery. Parse/validate the actualnode daemon runcommand after global options, and add a negative test for non-daemon arguments containing that pair. -
P2 —
ant-core/src/node/daemon/client.rs:531-532: an empty command list is treated as proof ofdaemon run. The surrounding comment says this is howsysinforeports unreadable arguments; the executable-name check still succeeds for another user’santprocess. A reused PID held by such a process is therefore reported as live, contrary to the changelog’s cross-platform claim. Represent unreadable identity separately and handle it without claiming the process is confirmed as the daemon; add a test for the unreadable-arguments case. -
P2 —
ant-core/src/node/daemon/client.rs:672-675: stale PID/port deletion errors are ignored, thenstartproceeds. If the stale port file remains (for example, deletion is denied), the readiness poll at lines 130-136 can immediately return that old port before the new daemon writes its port. Propagate or otherwise handle cleanup failure, and test that startup cannot report a stale port when cleanup fails.
Local targeted Rust tests did not return before the 420-second tool timeout; no local test pass is claimed. The PR’s GitHub checks are green. Coverage: 2/2 seats; no agreement is claimed.
…d fail on stale-file cleanup errors Second review of #235: - `daemon run` anywhere in the arguments was accepted, so `ant file upload daemon run` holding a reused PID passed. Only `node daemon run` straight after the executable, as `start` spawns it, now confirms the daemon. - Unreadable arguments fell back to the executable name. They are now "unconfirmed", like any other `ant` command line, and such a process counts as the daemon only if the daemon's API on the recorded port reports its PID. That also keeps a foreground daemon started behind global options recognised, without copying the CLI's option list into ant-core. - Errors removing the stale PID and port files were ignored, so the port poll could read the dead daemon's port back. `start` now fails instead. `start` also spawns the daemon only from the `ant` binary, and never from a unit-test build. Any other harness would read `node daemon run` as test-name filters and re-run the tests that spawned it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed in 01d5640:
Also: Verified:
CI on 01d5640: everything passes apart from Security Audit. Merkle E2E (ubuntu-latest) first failed on Security Audit: RUSTSEC-2026-0387 (crypto-bigint 0.5.5, via alloy/k256 0.13) is already on main and has no upstream fix yet; it's tracked in V2-1743. This PR doesn't touch |
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — re-review of 01d5640. Coverage: 2/2 seats (sol-1 gpt-6.1-sol xhigh: CHANGES_REQUIRED; glm-1 z-ai/glm-5.3: APPROVE). The seats disagree. I confirmed the remaining identity/API failure paths below against the source. The three prior findings are addressed: exact command matching, stale-file cleanup errors, and stale-port removal.
-
P2 —
ant-core/src/node/daemon/client.rs:579-584, 746-767(alsostop, lines 66-72): the API fallback returns onlybool; a failed connection, timeout, malformed response, and a responding server with another PID all becomefalse.recorded_daemonthen marks the still-live PIDStale, andstartdeletes both files;stopalso removes them when confirmation fails. For an actual daemon whose arguments are unconfirmed, a transient API timeout/refusal can therefore orphan the live daemon’s discovery files and allow a second start. Keep “not confirmed” distinct from “confirmed not daemon”; on an inconclusive probe, preserve files and return an actionable error rather than deleting them or signalling the process. Add tests for a live unconfirmed process with an unavailable/timed-out API. -
P2 —
ant-core/src/node/daemon/client.rs:592-605: the fallback always connects to IPv4127.0.0.1, butDaemonConfig.listen_addraccepts IPv6 addresses. A supported foreground invocation such asant --json node daemon run --listen-addr ::1is classified Unconfirmed because--jsonprecedes the subcommand; the fallback then probes the wrong address, classifies the live daemon as stale, andstartmay remove its files. Use the configured listener address (including the appropriate address family) and add a regression test for this invocation. -
P2 —
ant-core/src/node/daemon/client.rs:543-546(Windows):is_ant_executable_nameuses case-sensitive matching, andstartapplies it tocurrent_exe(). Windows executable paths are case-insensitive, so the same CLI installed/invoked asANT.EXEorAnt.exeis rejected before daemon startup. Compare case-insensitively on Windows and test mixed/upper-case executable names.
The PR’s Security Audit check is still failing on RUSTSEC-2026-0387 (crypto-bigint 0.5.5). Cargo.lock is unchanged from the base, so I treat this as an inherited check failure, not a new code finding; it remains a separate approval gate. All other listed checks pass.
…API on IPv6 too, and match `ANT.EXE` Third review of #235: - An unanswered daemon API counted as "not the daemon", so a live daemon whose API was briefly slow could lose its files to `start` or `stop`, and a second daemon could start. Confirmation now has three outcomes. Only an API answering with another PID rules the process out. With no answer, `start` and `stop` return an actionable error and remove or signal nothing, and `info` reports it as not running. - The API was only looked for on 127.0.0.1. It is now tried on the configured listen address (unspecified mapped to the loopback of its family), then on 127.0.0.1 and ::1. - On Windows the `ant` name check was case-sensitive, so `ANT.EXE` was rejected. It is now case-insensitive there, for `start` and for process identification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed in 7695a70:
Verified:
CI on 7695a70: everything else is green. E2E Tests (macos-latest) failed on the known Security Audit: still fails only on RUSTSEC-2026-0387, inherited from main (V2-1743). |
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — re-review of 7695a7006a10c66e11a058b96b4515c0062e0e0c. The earlier three findings are fixed: daemon argv matching, the IPv6 address fallback, and Windows case-insensitive executable matching. I found three remaining P2 issues:
-
ant-core/src/node/daemon/client.rs:717-730— macOS executable lookup errors are treated as proof that the process is notant.proc_pidpathreturning an error or zero returnsfalse;process_identitythen yieldsOther, sostartcan delete the PID/port files andstopcan clear them without stopping a live daemon. Preserve an unknown result when executable inspection fails; use API confirmation and do not delete/signal when that is inconclusive. Add a regression test for lookup failure. -
:643-648and:615-624— a different daemon answering on the first address prevents the IPv6 fallback.daemon_api_pidreturns the first valid PID response, even when it does not match the recorded PID.confirm_daemonthen treats that mismatch as proof the recorded process is not the daemon. If the recorded daemon listens on::1and another daemon answers on127.0.0.1at the same port, the IPv4 answer causes destructive cleanup without probing IPv6. Try all candidate addresses for the recorded PID; a different PID at one address must not rule out a match at another. Add a dual-stack, same-port test with different PIDs. -
:679-695— the one-second read timeout is not an overall response deadline.read_to_endcan keep receiving a slow trickle, with each read resetting the timeout, until the 64 KiB cap is reached. A local listener on a reused port can therefore hanginfo,start, orstopfor a long time. Enforce one end-to-end deadline across the request and response, and test a trickling response.
The review panel completed 2/2 seats and disagreed (sol-1: CHANGES_REQUIRED; glm-1: APPROVE). I verified the three findings against the source and resolve that dissent in favor of changes. This is a source review; I did not independently run Rust tests. GitHub reports the macOS E2E check still pending and Security Audit failing on the reported inherited advisory in unchanged crypto-bigint 0.5.5; those are separate from the code findings.
…unknown, and bound the status query - macOS and Windows: an executable that can't be looked up is unknown, not "not ant", so the process goes to the API check instead of losing its files. - The API check tries every candidate address until one reports the recorded PID. Another daemon answering on the same port is no longer proof that the recorded process isn't ours; it is inconclusive. - Each status query has one 1 s deadline across connect, request and response, so a trickling listener can't hold `info`, `start` or `stop`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed in 7be1709:
Verified:
CI on 7be1709: every job passed apart from the Security Audit, and no known flake came up. Security Audit: still fails only on RUSTSEC-2026-0387, inherited from main (V2-1743). |
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
APPROVE — reviewed head 7be1709756ffe324e2e617a29e24d7985f14ff59. The three findings from my prior review are fixed in this head: executable lookup errors remain unconfirmed, API probing continues until the recorded PID is found, and the status request has an end-to-end deadline.
Coverage: 2/2 review seats (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3). Both returned APPROVE; glm-1 completed on one narrowed retry after its initial request timed out. GitHub unit tests, E2E tests, builds, clippy and formatting pass.
Merge condition: Do not merge until Security Audit passes on the commit to be merged. It is currently failure on this head, with RUSTSEC-2026-0387 (crypto-bigint 0.5.5); Cargo.toml and Cargo.lock are unchanged in this PR. This approval is not a waiver of that gate and is not merge authorization.
#238 rotated the old [Unreleased] into the 0.3.7 to 0.3.12 sections, so this PR's entry moves to the new, empty [Unreleased], under the same heading it had before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Community report: after an unclean daemon exit,
ant node daemon infosaid the daemon was running (with a console URL that refused connections),statussaid it wasn't, andstartrefused to start a new one.Cause. A daemon that dies without cleaning up (a reboot, an OOM kill, SIGKILL) leaves
daemon.pidanddaemon.portbehind.infoandstartdecided "running" fromkill(pid, 0)alone. Once the PID was reused by an unrelated process, they treated it as the daemon.statusasks the daemon over HTTP, so it disagreed, and its "Start it with:ant node daemon start" hint went nowhere. Onlystopchecked that the PID really is the daemon.Fix.
infoandstartclassify the PID file through one helper (recorded_daemon): live and identifiable as the daemon, which is the checkstopalready applies before signalling.startremoves stale PID/port files before spawning. Otherwise its port poll reads the dead daemon's port straight back as the new daemon's.proc_pidpath, as Windows does), where it previously trusted any live PID. This also tightensstopon macOS.info's help text and README no longer say its output is "always JSON". It has been human-readable unless--jsonis passed since 920f872, which is the convention for every command. The e2e skill already usesinfo --json.statusandstoplogic are unchanged.Every distribution (install.sh, install.ps1, npm, release archives, the Linux .deb) names the binary
ant/ant.exe, which is what the identity check requires.Linear issue
Closes V2-1655
Risk tier
Compatibility
daemon.pid/daemon.portfiles; stale ones are now removed bystart)ant-coreitems added or changed;DaemonInfo.runningis now false for a stale PID file, which is what it documents.Semver impact
Test evidence
Unit tests (
cargo test -p ant-core --lib node::daemon::client): 12/12 pass on Windows and Linux. The new tests use the test process's own PID as the "reused" PID. The two tests for the reported case (info_with_a_reused_pid_is_not_running,start_clears_files_left_by_a_dead_daemon) fail with the old PID-only check restored, and pass with the fix.Lint:
cargo clippy --all-targets --all-features -- -D warningsclean on Linux;cargo fmt --checkclean.Reporter's scenario on Linux, real binaries, isolated
XDG_DATA_HOME: start a daemon,kill -9it, write a live unrelated PID intodaemon.pid.origin/maine47ea7ainfoon a genuine daemonrunning: truerunning: trueinfoafter kill -9 + reused PIDrunning: truerunning: falsestatusrunning: falserunning: falsestartalready_running: true, pid = reused PIDinfo/statusafterwardsmacOS is not run locally; the
proc_pidpathpath is compiled and tested by CI's macos-latest jobs.The e2e skill gains step 5.5, "Stale PID file recovery", which covers the same scenario.
Prod compat smoke (2026-10-23), re-run at the rebased head
da95adb5againstmainccd4106d. Run on Linux (dev2, stable 1.99.1) with--lockedrelease builds and isolatedXDG_DATA_HOME/XDG_CONFIG_HOME.mainand is fixed here:main, afterkill -9and a reused PID,inforeports the decoysleepPID as the daemon.startprintsalready running (PID <decoy>)andstatussays not running.infosays not running andstartlaunches a new daemon.infoandstatusthen agree on its PID, andstopexits cleanly.stoponmainalready catches the reuse:PID … is alive but does not appear to be the ant daemon (possible PID reuse). Stale PID file removed.This PR givesinfoandstartthe same check.ant file downloadof a public canary file (3cba93c8…, 5,277,908 B) with this build succeeded in 14.6 s, and the byte count matches.New dependency
none (
libcis already a dependency;proc_pidpathis from it)ADR
n/a
Mitigation / rollback
Revert the commit. If a renamed
antbinary fails the identity check, the impact is limited toinfo/startseeing its daemon as stale;stophas always refused such a daemon.🤖 Generated with Claude Code