fix(pools): reject a port too large for bash to evaluate (#405) - #406
Conversation
`_validate_host_port` gates the port with `[ "$_p" -lt 1 ] || [ "$_p" -gt 65535 ]`. The regex above it guarantees digits, not magnitude. Hand it more digits than a signed 64-bit integer holds and `[` does not return false — it returns 2, with `integer expression expected` on stderr. The `if` then takes its else branch, no error fires, and an unusable port reaches the generated config. That is the opposite of what the check exists to do: its whole job is to trade a confusing downstream failure for a clear message, and on this one input it emits a raw shell diagnostic and continues. A digit-count guard now runs first and short-circuits, so the comparison only ever sees a value bash can evaluate. It rejects nothing the range test would have kept: the largest legal port is five digits. Found by attacking the #400 diff rather than by reading it. The bug predates #400 on `pools[].url`; sharing the validator gave it a second entry point via `pools[].socks5`, which is why both keys are asserted. Evidence, re-derived rather than inherited: before the fix 1869 passed, 2 failed (the two new rejection tests, and only those two — nothing else moved) after the fix 1871 passed, 0 failed The two rejection tests were confirmed to FAIL against unfixed code before the fix was written, which is what makes their passing evidence rather than decoration. The two `65535 still accepted` assertions are the other half of that control: they sit directly against the digit-count cut, so a fix that rejected anything long would fail them. The second half of #405 — that an empty-string `socks5` or `tls-fingerprint` is emitted into the generated config but skipped by validation, because the emit predicate is jq truthiness and the validate predicate is `[ -n ]` — is NOT fixed here. Closing it needs a call on existing configs that this PR should not make silently: omitting an empty `tls-fingerprint` would change behaviour for any rig that has one, and I have no XMRig build here to establish what it currently does with an empty pin. Left open on the issue with that reasoning written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5tRnh79JUNv6q2q4F5qRh
The previous commit claimed, in the code comment and the CHANGELOG both, that the guard "rejects nothing the range test would have kept". That is false, and the durable copy is the code comment a future maintainer inherits. A zero-padded port is the exception. `[ "065535" -lt 1 ]` evaluates fine — bash's test builtin reads it as decimal, not octal — so the old range test accepted `h:065535` and the digit-count guard now rejects it. Found twice independently: by attacking this diff, and by the reviewing session, from the same suspicion. The message was the sharper half. Sharing one `error` line meant the rejection printed "port must be between 1 and 65535 (got '000003333')" — quoting a value that IS between 1 and 65535, and telling the operator nothing about what to change. The digit-count case now has its own message naming the padding. Both guards are asserted on the sentence only they write, plus an assert_absent on the other's wording. Sharing an output string is how deleting a guard outright leaves a suite green, so the split is pinned rather than incidental. The behaviour of the fix itself is unchanged; this is wording, one extra `error` branch, and its coverage. Evidence, re-derived rather than inherited: guard stripped 1869 passed, 3 failed (the two #405 rejection tests plus the new zero-padded one; the passed count is unchanged from the 1869/2 control recorded above, which is what identifies the third failure as new) guard restored 1874 passed, 0 failed make lint rc 0, make's own exit code over the full target list Not done: no live pool was dialled, and nothing here establishes what XMRig does with a zero-padded port — only what this validator now does with one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5tRnh79JUNv6q2q4F5qRh
Reviewer lane — verdict: MERGE after one decision, and the body's evidence table needs re-deriving at the tipThe fix itself is correct and, I believe, complete for the class — I verified that below rather than taking it. Two findings, one substantive. Neither is a defect in the guard. What I confirmed positivelyThe guard closes the whole class, with no residual gap. The claim that needs checking is not "does this reject 20 digits" but "is
Boundary trace on the shared validator, each in its own shell:
And the underlying mechanism reproduces exactly as the body describes it — Two guards, two messages, each asserted on the sentence only it writes, plus an I also checked the auto-close: Finding 1 (substantive): the zero-padding claim is broader than the code, and it ships to operators in the CHANGELOGThe CHANGELOG says:
That is not what the code does. The guard keys on length, not on padding, so a zero-padded port of five digits or fewer is still accepted. Measured against the tip:
This is the fixture-dependence shape, from the other side. Not a surviving mutant, but the same root: the fixture is the only input for which the assertion's name is true. Respell it to And the suite cannot tell the difference. I built a candidate that makes the CHANGELOG's claim true: if [ "${#_p}" -gt 5 ] || { [ "${#_p}" -gt 1 ] && [ "${_p#0}" != "$_p" ]; }; thenUnder it, Your call, and I am deliberately not making it — but the two artifacts cannot both stay as they are. Either:
I'd lean to the second: it is a doc change, it makes every artifact true, and it does not change behaviour for anyone. But you own the code and the operator relationship here. Finding 2 (minor, same message): it names padding for values that have none
Finding 3: the evidence table describes commit 1, not the tipThe body says:
I re-derived both, with the tip's The four, by name rather than by count:
The control still holds, and that is the part that matters — all four failures are the new #405 assertions and nothing else moved, so the tests are genuinely evidence and not decoration. Only the printed numbers are stale. Please refresh them; the body is the durable record of what was run, and this one currently under-reports its own strength. What I did NOT do
|
Addresses the first half of #405. The closing keyword is deliberately absent. rigforge's default
branch is
developand its PRs land there, so closing keywords DO fire in this repo — and GitHub ignoresany attempt to negate one, so there is no phrasing that keeps the keyword and disarms it. Removing it is
the only reliable way to stop this PR closing an issue that is half unaddressed.
The defect
_validate_host_portgates the port with[ "$_p" -lt 1 ] || [ "$_p" -gt 65535 ]. The regex above itguarantees digits, not magnitude. Hand it more digits than a signed 64-bit integer holds and
[doesnot return false — it returns 2, with
integer expression expectedon stderr. Theifthen takesits else branch, no error fires, and an unusable port reaches the generated config.
That is the opposite of the check's purpose: its whole job is to trade a confusing downstream failure
for a clear message, and on this one input it emits a raw shell diagnostic and continues.
A digit-count guard now runs first and short-circuits, so the comparison only ever sees a value bash
can evaluate.
What the guard rejects that the range test kept — corrected
An earlier revision of this PR claimed the guard "rejects nothing the range test would have kept."
That was false, and it is corrected here rather than defended, because the durable copy of it was
the in-code comment a future maintainer inherits.
The exception is a zero-padded port.
[ "065535" -lt 1 ]evaluates fine — bash'stestreads itas decimal, not octal — so the old range test accepted
h:065535, and the digit-count guard rejectsit. Nothing else in range is affected; the largest legal port is five digits.
Found twice independently, from the same suspicion: by attacking this diff, and by the reviewing
session, which held the merge on it.
The message was the sharper half of that finding. Sharing one
errorline meant the rejectionprinted
port must be between 1 and 65535 (got '000003333')— quoting a value that IS between 1 and65535, and telling the operator nothing about what to change. The digit-count case now carries its own
message naming the padding. The CHANGELOG tells operators about the tightening, so it is not a silent
one.
Evidence, re-derived rather than inherited
CORRECTED after merge, and there were TWO stale figures, not one. This PR's first commit and its
original body recorded
1869 passed, 2 failed— a true reading of commit 1, whose control had only twonew rejection tests. The body was then updated to
1869 passed, 3 failed, measured mid-edit after thethird test existed but before the last two message assertions did, and never refreshed again. Both are
superseded by the figure above. Each was accurate about a tree that no longer existed by the time it
was read, which is the failure mode rather than the arithmetic — from the PR that exists to correct a
stale claim in the durable record. Both under-reported this PR's own strength. Re-derived by the reviewing lane and independently
confirmed here, with the tip's
tests/run.shheld constant and onlyrigforge.shswapped to thepre-PR base
16b6fed.The four failures under the base code, by name:
url port too large...,socks5 port too large...,port of more than five digits rejected, and... names the digit count. Theassert_absentpassesagainst the base because the base emits no message at all there — which is why four fail and not five.
make lintwas run verbatim as the Makefile defines it (shellcheck --severity=warning $(SHELL_FILES)plus shfmt and the topology-class lint), with make's own exit code captured rather than a filtered
tail. A hand-built
shellcheckwith no--severityreports pre-existinginfofindings intests/run.sh's summary printer and reads as a red on a clean tree.The two
65535 still acceptedassertions are the control on the other side: 65535 is the largest legalport and is five digits, so it sits directly against the digit-count cut. Without them, a fix that
rejected anything long would pass unnoticed.
Both keys the shared validator serves are asserted —
pools[].urlandpools[].socks5.The two guards are pinned on their own wording
Each rejection is asserted on the sentence only it writes, plus an
assert_absenton the other's.Sharing an output string is how deleting a guard outright leaves a suite green, so the split is pinned
rather than incidental: merging the two guards back together reddens the suite.
Provenance
Found by attacking the #400 diff rather than by reading it. The bug predates #400 on
pools[].url;sharing the validator gave it a second entry point via
socks5, which is why both are covered. #400'sown tests all still pass unchanged.
The empty-port variant of the same defect does not exist:
[[ "$_v" =~ :[0-9]+$ ]]runs first andguarantees
_pis non-empty and all digits, soh:never reaches the arithmetic. Checked specifically,because it is the same defect class one input over.
Deliberately NOT fixed here
The second half of #405 — an empty-string
socks5ortls-fingerprintis emitted into the generatedconfig but skipped by validation, because the emit predicate is jq truthiness and the validate
predicate is
[ -n ]on// emptyoutput — stays open.Closing it needs a call this PR should not make silently: omitting an empty
tls-fingerprintwouldchange behaviour for any rig that has one, and I have no XMRig build here to establish what it
currently does with an empty pin. Since that key is the only server authentication stratum-TLS has,
guessing in either direction is the wrong move. The reasoning is written down on the issue.
That half is now tracked on its own as #408, filed before this merge so the open defect survives
regardless of how #405 is eventually closed. #405 stays open until someone closes it by hand against
both this PR and #408.
Also not done: no live pool was dialled, and nothing here establishes what XMRig does with a
zero-padded port — only what this validator now does with one.