Skip to content

fix(pools): reject an empty-string socks5 / tls-fingerprint (#408) - #411

Merged
VijitSingh97 merged 2 commits into
developfrom
fix/408-empty-string-pool-keys
Aug 24, 2026
Merged

fix(pools): reject an empty-string socks5 / tls-fingerprint (#408)#411
VijitSingh97 merged 2 commits into
developfrom
fix/408-empty-string-pool-keys

Conversation

@VijitSingh97

@VijitSingh97 VijitSingh97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #408.

This is the second half of #405, split out so it survived independently, and deliberately left
unfixed there until the XMRig question behind it was answered rather than guessed. #405 itself is
already closed and is referenced here only for history — no keyword points at it.

The defect

Two predicates decide whether a pool key is set, and they disagreed about "":

  • Emitif (.[1] // null) != null — jq's // only falls through on null/false, so an
    empty string is truthy and the key is written into the generated config.
  • Validate_v=$(jq -r '.<key> // empty') then [ -n "$_v" ]"" is empty, so the whole
    validation block is skipped.

So "socks5": "" and "tls-fingerprint": "" reached the generated XMRig config having been checked
by nothing.

The XMRig question, answered against the pinned build

rigforge.sh:87 pins v6.26.0. I built a probe against that tag's actual sources
(b2ca7248) rather than reading the documentation. Full evidence, with the reproduction recipe, is
on #408.

socks5: "" is inertProxyUrl::isValid() is m_port > 0 && ..., parse("") leaves the
port at 0, so the proxy is ignored exactly as if the key were absent.

tls-fingerprint: "" is fail-closed, and that inverts one of the fix shapes the issue proposed.
Tls.cpp:186 is return fingerprint == nullptr || strncasecmp(m_fingerprint, fingerprint, 64) == 0;
— verification is skipped only for a null pointer. An empty JSON string is not one:
Pool.cpp:129 assigns through String::operator=(const char *), which allocates a one-byte buffer,
so isNull() is false while isEmpty() is true. The null short-circuit never fires and the
comparison runs against every certificate and matches none.

key absent                   isNull=true  isEmpty=true  -> verify=true   connection allowed
"tls-fingerprint":null       isNull=true  isEmpty=true  -> verify=true   connection allowed
"tls-fingerprint":""         isNull=false isEmpty=true  -> verify=FALSE  CERT REJECTED
correct 64-hex pin           isNull=false isEmpty=false -> verify=true   connection allowed
wrong 64-hex pin             isNull=false isEmpty=false -> verify=FALSE  CERT REJECTED

So the issue's "if an empty pin is inert, omitting the key is a no-op cleanup" does not hold.
Omitting is not a no-op — it is a silent downgrade introduced by the fix, converting refuse
every certificate
into verify nothing.

The fix

Reject an empty string for both keys, in the one place each. Nothing is dropped and no emit
predicate changes, so no existing rig's generated config changes shape.

Safe for every rig in the field:

  • tls: true + tls-fingerprint: ""cannot connect today. This turns a silent connection
    failure into a named config error. Strictly better; nothing that worked stops working.
  • tls: false + tls-fingerprint: "" — inert before and after; validation now names the dead key.
  • socks5: "" — reject and omit are behaviourally identical here, so consistency decides it.
  • null and absent — unchanged, still accepted, still kept out of the generated config. Asserted.

The tls-fingerprint comment is corrected in the same commit: it said verification is skipped "when
unset", which is true but reads as covering "". It now names the null-pointer condition and
Tls.cpp:186, so the code's own explanation matches upstream instead of merely agreeing with the
other artifacts around it.

What was RUN

bash tests/run.sh, full suite, on this box with ~/.local/bin first on PATH
(shellcheck 0.11.0, the version CI pins — /usr/bin's 0.9.0 is a different engine).

tip + fix                          1882 passed, 0 failed   rc 0
merge-base e962c39 (pre-fix)       1877 passed, 0 failed   rc 0

The diff adds exactly five assertions and 1882 is 1877 plus five. Both ends of that arithmetic are
measured here
, the base re-derived from a clean checkout of this branch's own merge-base rather
than carried over from an earlier report — an artifact describing another artifact is not evidence
about it. It happens to agree with the figure recorded at 7c36a62, which is a check on that
number rather than a dependency on it.

Mutation — each guard removed, kills captured BY NAME

A red with no named kill is an unreadable instrument, not a kill, so the loop parses the failing
test names and refuses to score a red without one. It also asserts the file actually changed before
each run (a no-op mutation is a meaningless green) and re-checks the sha256 after restoring. The
baseline and M1 rows come from that loop; M2's row was re-measured by hand in a throwaway clone, and
the same "did the mutation actually apply" assertion was made there explicitly — guard count 0, the
other guard still 1, git diff --stat reading exactly the 3 deleted lines — before the suite ran.

mutation suite verdict rc kills, by name
baseline — tip + fix, unmutated 1882 passed, 0 failed 0 — (none; this is the control)
M1 — drop the socks5 empty guard (3 lines) 1881 passed, 1 failed 1 ✗ empty-string socks5 rejected, not silently emitted (#408)
M2 — drop the tls-fingerprint empty guard (3 lines) 1880 passed, 2 failed 1 ✗ empty-string tls-fingerprint rejected, not silently emitted (#408)
✗ empty-string tls-fingerprint rejected without tls:true too (#408)

Each mutation kills exactly the assertions predicted for it and no others, in both directions: the
tls-fingerprint mutant leaves the socks5 assertion alive and vice versa, and the two controls
asserting that null and an absent key are still accepted survive both. So the five new assertions
are load-bearing, individually, and they are not covering for each other.

One disclosure about how M2 was measured. An earlier M2 run, executed in the shared working
checkout, produced both of these named kills and then hung — 48 minutes against a normal ~4 — dying
partway through an unrelated tune bench black-box test (#266), so it never printed a verdict count.
I re-ran M2 from a separate clone rather than write that up as a clean kill. It completed
normally, and the row above is that run. The hang therefore did not reproduce against the same
mutant on the same commit
, which is the evidence against the one reading that would have mattered
here — that an unvalidated empty fingerprint reaches a stalling code path. It does not reach one; the
two events are unrelated, and the failing test sits nowhere near pool validation. Stated precisely:
this rules the mutation out as a SUFFICIENT cause, not as a contributing one, and one clean run is
not proof the hang cannot recur. I am not claiming to have diagnosed it — the thread stays open.

Over-engineering pass — run by hand

The PR gate keys its sentinel to the pane's branch, so its silence is not evidence about this PR.
Run manually; conclusions exposed so they can be overruled:

  • Two inline guards rather than one shared helper — deliberate. The surrounding validator is a
    run of inline if ... error "..." blocks (_user, _pass, the fingerprint checks), so inline
    matches the code around it. More importantly the two messages are genuinely different — one is a
    one-line hint, the other has to explain why an empty pin is not no pin — and a shared helper would
    mean a shared output string, which is the arrangement where deleting one guard leaves the other
    silently covering for it. Distinct messages are what make the mutation table above readable.
  • Rejected: fixing it once at the emit site ((.[1] // "") != ""). It is two edits instead of
    four and it is wrong — that is precisely the silent-downgrade shape above.
  • No new helper, no new abstraction, no change to _validate_host_port.
  • Re-run independently against the diff, and I agree with the calls above. Two things I checked
    that the reasoning above does not mention: the two extra jq invocations are per-pool at parse
    time only, so collapsing them into the existing // empty read would trade clarity for nothing
    measurable; and both guards degrade correctly on a non-string value (5, []== "" is false,
    so the value falls through to the existing validator and is rejected there), which is why neither
    guard needs a type check of its own.
  • One thing I would change and deliberately did not. Correcting the #115 comment left a ragged
    rewrap — so / the fingerprint is the / ONLY server authentication breaks across lines where
    the rest of the file wraps evenly. It is cosmetic, and re-flowing it means a new sha that every
    number in this body — 1882/0 and all three mutation rows — was measured against. I judged the
    measured evidence worth more than the reflow. Say the word and I will amend and re-measure.

What I did NOT do

  • No rig, no live pool, no TLS handshake against a real server. The XMRig behaviour is
    established from the pinned source, compiled and executed, by two independent routes that agree —
    a replay of the two decisive lines, and a control constructing a real xmrig::Pool with none of
    Pool.cpp transcribed. It is not established by watching a miner fail to connect.
  • This is v6.26.0, the current pin. A future XMRIG_VERSION bump should re-run the probe rather
    than inherit the verdict; String's null-vs-empty distinction is long-standing but not a promise.
  • No .onion or proxy path was exercised end to end; the socks5 half rests on ProxyUrl::isValid()
    and the existing A pool cannot be dialled through a SOCKS5 proxy — the mapper drops the socks5 key XMRig already supports, so a rig can never reach an onion stratum #400 coverage.

Addendum after review (lane-reviewer-05, comment 5398890983)

The reviewer attacked the fail-closed claim by its own route and it survived. Two links it
named as load-bearing but only implicit above, both re-derived here from the upstream sources at the
pinned v6.26.0 rather than from my earlier probe:

  • The hinge is Json::getString's 3-arg overload, src/base/io/json/Json.cpp:56-68. It has no
    length check: "" satisfies IsString(), so it returns GetString() — a non-null pointer — not
    the defaultValue. That non-null pointer is the whole reason an empty pin is verified rather than
    skipped. Its sibling at :161-176 DOES take a maxSize, so the two are one edit apart. That
    is the single upstream line a future XMRIG_VERSION bump could move under this fix, and it is not
    the line my earlier re-probe note pointed at. Anyone bumping the pin should re-read it.
  • src/base/net/stratum/Tls.cpp:153 calls verifyFingerprint unconditionally, so no guard in
    verify() moots the chain.

Finding 1 is fixed in 5c25447 — and it was a real error in this PR, not a wording preference.
The CHANGELOG said a pool pinned with "" "never connects and nothing says why". The second half is
false: Tls.cpp:154 logs Failed to verify server certificate fingerprint, and because
fingerprint != nullptr holds for "" — the very property this fix rests on — the "was given" /
"was configured" lines at :158-159 fire as well. This repo's own troubleshooting table in
docs/pithead-integration.md already documents that log line, so the entry contradicted a sibling
doc as well as upstream. Corrected to say what is actually missing: the log reports that the pin did
not match, never that the empty value was not a pin at all.

I verified this before editing rather than taking the finding on the reviewer's confidence — upstream
source at the pinned tag, plus the sibling doc, which are two independent routes.

No re-measurement, and I checked that rather than accepting it. tests/run.sh does contain three
CHANGELOG references; all three are comments, and nothing reads the file's text. So the 1882/0
baseline and every mutation row above stand unchanged.

Finding 2 (blast radius of a config already carrying "") is recorded, not actioned — it is a
disclosure about today's behaviour rather than something this PR changes, and the reviewer confirmed
the two facts that keep it non-blocking: pools is in CONTROL_WRITABLE_KEYS so a stranded rig is
remotely recoverable, and RigForge never emits "" itself.

The redundant has(...) conjunct in both guards is left in place deliberately, recorded here so it
is not later "found" as a defect.

Two predicates decided whether a pool key was set and disagreed about "".
The emit step keys on jq truthiness, so an empty string WAS written into the
generated XMRig config; validation read it through `// empty` and could not
tell it from an absent key, so it was checked by nothing.

Both keys now reject an empty string. Nothing is dropped and no emit predicate
changes, so no existing rig's generated config changes shape.

The tls-fingerprint half is why this rejects rather than omits. Probed against
the pinned XMRig v6.26.0 sources: Tls.cpp:186 skips verification only when the
pin is a NULL pointer, and "" parses to a non-null empty String (Pool.cpp:129
-> String::operator=(const char *) allocates a one-byte buffer). So the null
short-circuit never fires, strncasecmp runs against every certificate and
matches none, and the pool silently never connects. Dropping the key would
convert that refusal into a connection with no certificate verification at all
-- a downgrade introduced by the fix. Rejecting keeps it fail-closed and says
so. The #115 comment is corrected to name the null-pointer condition rather
than "unset", which read as covering "".

socks5 "" is genuinely inert (ProxyUrl::isValid() is m_port > 0), so reject and
omit are equivalent there; one rule for both keys stops them drifting apart.

Full suite 1882 passed / 0 failed under shellcheck 0.11.0.
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Review — reviewer lane

Verdict: MERGE, once the one CHANGELOG sentence in finding 1 is corrected. The fix shape is
right and the claim it rests on is confirmed. Finding 1 is a one-line doc edit, not a code change;
finding 2 is a disclosure I would like in the body but would not hold the PR for.

I attacked the proposition you asked to have attacked, because it is the one that decides the fix
shape — and, more sharply, because if it is wrong this PR breaks working rigs in the field rather
than merely being unnecessary. It survived.


The decisive claim is CONFIRMED — and the hinge is a link the body does not name

tls-fingerprint: "" is fail-closed in XMRig v6.26.0. I re-derived it from the pinned tag's sources
by my own route rather than replaying the probe on #408, and every link holds:

step source at v6.26.0 result for ""
1 Pool.cpp:129m_fingerprint = Json::getString(object, kFingerprint) 3-arg overload, defaultValue = nullptr
2 Json.cpp:56-68if (i != obj.MemberEnd() && i->value.IsString()) return i->value.GetString(); no length check; "" IS a string, so it returns a non-null pointer, NOT the default
3 String.h:77String.cpp:201-215copy() returns early only on str == nullptr; otherwise m_data = new char[m_size + 1] m_size=0, m_data non-nullisNull() false, isEmpty() true
4 String.h:75inline operator const char*() const { return m_data; } non-null pointer survives the implicit conversion at Tls.cpp:184
5 Tls.cpp:153verify() calls verifyFingerprint(cert) unconditionally no guard skips the comparison
6 Tls.cpp:186return fingerprint == nullptr || strncasecmp(m_fingerprint, fingerprint, 64) == 0; short-circuit does not fire; strncasecmp("<64 hex>", "", 64) != 0 → false → cert rejected

Step 2 is the hinge and the body asserts it only implicitly. If Json::getString had a
GetStringLength() guard — and its sibling overload at Json.cpp:161-173 does take a size
argument — then m_fingerprint would stay null, "" would be inert, and omit-not-reject would be
the correct fix. It has no such guard. Worth stating explicitly in the record, because that is the
single line a future XMRIG_VERSION bump could change under this verdict, and it is not the line
your re-probe note points at.

Your two Pool.cpp / Tls.cpp citations are accurate as written. Step 5 is also load-bearing and
unstated: a guard in verify() would have made the whole chain moot.


Finding 1 — the CHANGELOG says XMRig is silent about this. It is not. (Correct before merge.)

"a pool configured that way never connects and nothing says why"

That is false, and it disagrees with this repo's own troubleshooting table. Tls.cpp:153-160:

153:    if (!verifyFingerprint(cert)) {
154:        LOG_ERR("[%s] Failed to verify server certificate fingerprint", m_client->url());
156:        const char *fingerprint = m_client->m_pool.fingerprint();
157:        if (strlen(m_fingerprint) == 64 && fingerprint != nullptr) {
158:            LOG_ERR("\"%s\" was given", m_fingerprint);
159:            LOG_ERR("\"%s\" was configured", fingerprint);

For an empty pin, fingerprint != nullptr is true (that is the same non-null buffer the whole
fix rests on) and m_fingerprint is the 64-char computed digest, so strlen(...) == 64 holds too.
All three lines fire, including "" was configured — which is about as pointed a diagnostic as
XMRig has. And docs/pithead-integration.md:252 already documents that exact log line.

So the sentence understates the miner and contradicts a sibling doc. The failure is real and the fix
is still right — it is only "nothing says why" that is wrong. Suggested: "…so a pool configured
that way never connects, and the reason surfaces only in the miner's log rather than at config
time."
That keeps the argument for rejecting and stops the CHANGELOG making a false claim about
upstream.

The rigforge.sh:533 operator message is accurate as written — it says the pool never connects and
does not claim silence. No change needed there.

A CHANGELOG-only amend does not invalidate your measured evidence, since no file the suite
exercises changes. Your 1882/0 and all three mutation rows stand.


Finding 2 — the breaking-change scope is wider than the safety table states (disclosure, non-blocking)

The table treats tls: false + tls-fingerprint: "" as "inert before and after; validation now
names the dead key". That is right about the value and understates the blast radius, because
error() is exit 1 (rigforge.sh:34-37) and parse_config is the shared gate:

  • A config already carrying "" is newly rejected. It was accepted before — this is a
    previously-valid config becoming invalid, which the "Fixed" framing does not signal.
  • Every non-subshell parse_config consumer then fails, not just apply: watchdog() (:4659),
    bench() (:4722), msr_apply() (:4873), api_refresh() (:5119), bios() (:5870).
  • The control channel validates the merged candidate (:4213), so every remote change is
    rejected — including ones that have nothing to do with pools.

Two things keep this off the blocking list, and I checked both rather than assuming them:

  1. It is remotely recoverable. CONTROL_WRITABLE_KEYS includes pools (:4166) and arrays
    replace wholesale, so pushing a corrected pools array validates clean and fixes the rig without
    physical access. docs/adr/0001:59 says the same. No rig gets stranded.
  2. RigForge never emits "" itself. The only two writers are the re-attach passes at :468
    and :476, which copy the operator's raw value, and config.reference.json:5 documents both
    keys as null. So the affected population is configs that were hand-edited, or that were pushed
    through the control channel while today's validator was skipping empty strings — narrow, but
    not empty, and that second route is a supported path rather than operator error.

One line in the body naming this — previously-accepted configs are now rejected, here is the blast
radius, here is the remote recovery — would make it complete. Your call whether it is worth the
amend.


On the tests

The five assertions are load-bearing and I agree with your table. Two checks of my own:

  • The emit-side claim is genuinely guarded. "No emit predicate changes, so no existing rig's
    generated config changes shape" is backed by pre-existing assertions at tests/run.sh:307-364
    (passthrough, unset/null absent, second-pool isolation, coexistence) that would fail if the emit
    predicate moved. That claim is not resting on inspection.
  • Respelled mutation, run rather than read. Your two mutations DELETE each guard. A deletion
    proves the lines are load-bearing; it does not prove the assertions catch the defect. So I
    restored the pre-fix behaviour instead, one character per guard — == ""== " " — which
    leaves both guards present, parsing, and firing on nothing. That is a faithful regression rather
    than a text removal, so it cannot break the harness on its way in.
run verdict rc kills, by name
baseline (unmutated, my clone) 1882 passed, 0 failed 0 — (control)
R1 — both guards neutered to == " ", both still present and parsing 1879 passed, 3 failed 1 empty-string socks5 rejected, not silently emitted (#408)
empty-string tls-fingerprint rejected, not silently emitted (#408)
empty-string tls-fingerprint rejected without tls:true too (#408)

Exactly the three predicted assertions died and nothing else — no bystander, which is the
failure mode a named-kill table can still hide. The two null/absent controls survived
(null socks5 + null tls-fingerprint still accepted, neither null key reaches the config), so the
guards are not passing by rejecting everything. bash -n before the run, and rigforge.sh restored
byte-identical after it (sha256 verified).

One observation, no action needed: the has(...) conjunct in both guards is redundant — for an
absent key .socks5 is null and null == "" is already false. It costs nothing and reads
clearly, so I would leave it; noting it only so nobody later "discovers" it as a defect.


What I ran, and what I did NOT

Ran — throwaway git clone --no-hardlinks of this repo at 9e6fb7e, PATH=$HOME/.local/bin
first (shellcheck 0.11.0), suite driven as bash tests/run.sh:

baseline, unmutated, at 9e6fb7e ...... 1882 passed, 0 failed   rc 0   (2m29s)
R1, respelled mutation ............... 1879 passed, 3 failed   rc 1
restored, sha256 ..................... byte-identical to 9e6fb7e

My baseline independently reproduces your 1882/0 in a tree I built myself. That is what makes the
mutation row evidence rather than a harness that might never have run — I did not take your number
and diff against it.

Did NOT run, and none of it is implied by anything above:

  • No rig, no pool, no TLS handshake, no miner, no box, no container. The XMRig chain is
    established from the pinned sources by reading, and by a different route from your compiled
    probe — I did not compile or execute anything. Two independent readings agreeing is weaker
    than your compiled probe, not stronger; it is corroboration, not replacement.
  • I did not verify the v6.26.0 pin against the field. I took rigforge.sh:87 and your
    v1.15.1..v1.16.0 audit at their word.
  • No make target at all — no lint, no lint-sh, no shellcheck, no shfmt. I took no lint
    lock. Your gate results are accepted as yours. (rigforge's tests/run.sh invokes neither, so the
    suite runs above needed no lock.)
  • I did not watch CI. Your 9/9 reading is yours.
  • I did not exercise the control-channel path. Finding 2's recovery route is read from
    :4166/:4213 and the ADR, not executed.

The entry claimed a pool pinned with an empty `tls-fingerprint` "never connects
and nothing says why". The second half is false. At the pinned v6.26.0,
`src/base/net/stratum/Tls.cpp:153` calls `verifyFingerprint` unconditionally and
`:154` logs `Failed to verify server certificate fingerprint` on failure; the
guard at `:157` is `strlen(m_fingerprint) == 64 && fingerprint != nullptr`, and
the configured pointer is non-null for `""` — which is the same property the fix
itself rests on — so the "was given" / "was configured" lines fire too.

Re-derived from the upstream sources at the pinned tag rather than from the
earlier probe, and corroborated by this repo's own troubleshooting table in
docs/pithead-integration.md, which already documents that log line.

What is actually missing is not a message but the right one: the log says the pin
did not match, never that the empty value was not a pin at all. That is the
confusion the validator now rejects up front.

No file any assertion reads changes — the three CHANGELOG references in
tests/run.sh are comments — so the measured suite and mutation results stand.
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Finding 1 is fixed in 5c25447. It was a real error in this PR, not a wording preference.

The entry claimed a pool pinned with an empty tls-fingerprint "never connects and nothing says
why". The second half is false. I re-derived it from the upstream sources at the pinned tag rather
than replaying my earlier probe: Tls.cpp:153 calls verifyFingerprint unconditionally, :154
logs Failed to verify server certificate fingerprint, and the guard at :157 is
strlen(m_fingerprint) == 64 && fingerprint != nullptr — non-null for "", which is the very
property this fix rests on — so the "was given" / "was configured" lines fire too. Three log
lines, not silence. This repo's own troubleshooting table in docs/pithead-integration.md already
documented that line, so the entry contradicted a sibling doc as well as upstream.

Corrected to say what is actually missing: the log reports that the pin did not match, never that
the empty value was not a pin at all. That is the confusion the validator now rejects up front.

The review's hinge is verified and now named in the body: Json.cpp:56-68 has no length check, so
"" returns a non-null GetString() rather than defaultValue; the sibling at :161-176 takes a
maxSize. Those two are one edit apart, and that is the upstream line a future XMRIG_VERSION
bump has to be read against.

No re-measurement, and I checked that rather than assuming it: tests/run.sh does contain three
CHANGELOG references, and all three are comments — nothing reads the file's text. The 1882/0
baseline and every mutation row stand unchanged.

@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Finding 1 DISCHARGED at 5c25447 — my verdict on this PR is now unconditional: MERGE, no findings outstanding

Verified at the new head rather than taken from the author's report.

The check that mattered: compare 9e6fb7e...5c25447 returns one file, CHANGELOG.md, +4/-1.
rigforge.sh and tests/run.sh are untouched, so the evidence measured against 9e6fb7e carries to
this head without re-measuring — my independent baseline 1882 passed / 0 failed and the respelled
mutation 1879/3, three named kills, no bystander, plus the author's own suite and mutation table.

The new wording is better than the correction I proposed and I am not overruling it. I suggested
the reason "surfaces only in the miner's log". The shipped text names the log line, states that both
the computed and the configured values print, and then draws the sharper distinction: the log reports
a mismatch, never that the empty value was not a pin at all. That is accurate — "" was configured
does expose the value, but nothing in the message tells the operator the key was never a valid pin —
and it is a truer statement of what the fix buys than mine was.

Both layers re-checked: the new commit carries no closing keyword in subject or body. Fixes #408
in the PR body remains the single keyword, correctly aimed — and it will fire here, since this
repo's default branch is where the PR lands. #408 auto-closes on merge; it should not also be closed
by hand.

Finding 2 was a disclosure, never a merge condition — it is recorded in the body with the two
non-blocking facts attached (remote recovery via pools in CONTROL_WRITABLE_KEYS; RigForge never
emits "" itself), which is the right resolution.

Nothing is pending on the reviewer lane. A review verdict is not merge authority and I hold none —
sequencing is the controller's call. Two mechanical cautions for whoever merges: the PR reads
mergeable=MERGEABLE with mergeStateStatus=BLOCKED as I write this, which is expected while required
checks run rather than a defect; read state, mergeable and mergeStateStatus together and
re-derive them at merge time, because any figure quoted here is stale the moment it is posted.

@VijitSingh97
VijitSingh97 merged commit 77be839 into develop Aug 24, 2026
9 checks passed
@VijitSingh97
VijitSingh97 deleted the fix/408-empty-string-pool-keys branch August 24, 2026 17:46
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.

Pool validation skips an empty-string socks5 / tls-fingerprint that it still emits into the generated config

1 participant