fix(mcp): reject daemon port 0 at the flag and the environment variable - #290
Merged
StefanSteiner merged 1 commit intoSep 7, 2026
Merged
Conversation
`--port` promises an exact bind, and `"0".parse::<u16>()` succeeds, so `0`
passed validation at both entry points and reached `TcpListener::bind`, which
assigns an *ephemeral* port rather than port 0.
The failure compounded instead of surfacing. With the resulting
`PortScan { base: 0, span: 1 }`, every client's scan probed port 0, got a
connection error, and read `ProbeResult::Refused` as "this port is free" — so
each client that missed the discovery fast path concluded no daemon existed
and started another daemon-and-hyperd pair, on another ephemeral port no scan
could ever find. The pairs accumulated silently.
`--port` now carries a `1..` range, so clap reports a usage error and exits 2,
and the `HYPERDB_DAEMON_PORT` chain filters `0` into the same default fallback
that unparseable values already take.
The CLI test drives the real binary, because `Cli` lives in `main.rs` and is
not reachable from a test crate. It waits on each child against a deadline and
kills it rather than calling `output()`, and pins HOME/USERPROFILE/
HYPERDB_STATE_DIR into a temp dir. Both guards are there because running it
against the unfixed binary hung the suite outright — `daemon --port 0` was
accepted and ran a foreground daemon forever — and that daemon published
`daemon.json` over the developer's live discovery record. A regression now
fails with a message, inside the sandbox.
Fixes tableau#275
This was referenced Sep 6, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #275
Summary
4 of #275's 5 items were already fixed on
mainby PRs #278 (landed inside #286's squash,b54103c) and #279 (56bc0d0). I audited each against the code and posted the item-by-item evidence as a comment on the issue. This PR closes the one that was genuinely still live: item 3, port 0.For reviewers who want the short version of the audit:
b54103c—remove_fileand the false Windows-rename premise both gone56bc0d0(the polling-side retry this issue asked for) and #278 /b54103c(writer made atomic +wait_for_reported_pid_retries_past_a_torn_write)Malformedb54103c—RawDiscoveryRead::Oversized, doctor now says "valid but larger than any legitimate record should be", test renamed to assert the honest classificationb54103c—UnexpectedEof, plus the same fix in the doctor's own read loopItem 2 is worth calling out because #278's own description said it fixed the torn read "at the writer" and left the polling side open. It didn't need to — #279 had already added the polling-side retry. Both halves are in, and both should stay: the retry is what stops a writer that ever regresses to a plain
fs::writefrom resurrecting the flake.The fix (item 3)
--portpromises an exact bind, and"0".parse::<u16>()succeeds, so0passed validation at both entry points and reachedTcpListener::bind— which assigns an ephemeral port rather than port 0.What makes this more than cosmetic is that the failure compounds instead of surfacing. With the resulting
PortScan { base: 0, span: 1 }, every client's scan probes port 0, gets a connection error, andProbeResult::Refusedreports it free. So each client that misses the discovery fast path concludes no daemon exists and starts another daemon-and-hyperdpair, on another ephemeral port no scan can ever find. The pairs accumulate silently.Both entry points now reject it, exactly as the issue proposed:
#[arg(long, global = true, value_parser = clap::value_parser!(u16).range(1..))]. clap reports a usage error and exits 2..filter(|port| *port != 0)in theHYPERDB_DAEMON_PORTchain, so0takes the same default fallback that unparseable values already take.The
--porthelp text andresolve_port_scan's rustdoc now both say why, rather than leaving the next reader to rediscover thatbase: 0, span: 1is unsatisfiable by construction.Two things I learned by running the test against the unfixed binary
The CLI test drives the real binary, because
Clilives inmain.rsand is not reachable from a test crate. My first version usedCommand::output(), and running it against the unfixed binary hung the suite outright —daemon --port 0is accepted and runs a foreground daemon forever, sooutput()never returns. That same daemon then publisheddaemon.jsonover my live developer discovery record.Both are fixed in the test, and I think both are worth keeping as stated requirements rather than incidental hygiene:
try_wait()and killed if it outlives it, so a reverted fix fails with a message naming what happened instead of wedging CI.HOME,USERPROFILEandHYPERDB_STATE_DIRare pinned into aTempDir, and the test asserts nodaemon.jsonwas published — which is what actually caught the regression when I ran the red-proof, before the exit-code assertion got a chance to.This is a small argument for the same kind of sandboxing in any future test that shells out to the
daemonsubcommand.Verification
Isolated worktree, dedicated
CARGO_TARGET_DIR,HYPERD_PATH=~/dev/bin/hyperd.cargo fmt --all -- --check— exit 0, no diff.cargo clippy --workspace --all-targets --all-features -- -D warnings— exit 0, no warnings.cargo test -p hyperdb-mcp— 645 passed, 0 failed, 16 ignored, exit 0.RUSTDOCFLAGS="-D warnings" cargo doc --no-deps— exit 0.npx markdownlint-cli2— 68 files linted, 0 issues, exit 0.Red-before-green, with the
value_parserrange and the.filtereach reverted:Both green after restoring the fix, and confirmed the bounded kill left no stray daemon or
hyperdprocess behind.Relationship to the other PRs in this batch
Independent of #289 (issue #274, the accept loop) — that PR touches
health.rs, this one touchesmain.rsanddiscovery.rs, so they do not overlap and either can merge first. Both are based on2f31b9e.