Skip to content

server: sweep dstNodes — unauthenticated unbounded map growth - #51

Merged
TeoSlayer merged 2 commits into
mainfrom
fix/dstnodes-unbounded-growth
Jul 29, 2026
Merged

server: sweep dstNodes — unauthenticated unbounded map growth#51
TeoSlayer merged 2 commits into
mainfrom
fix/dstnodes-unbounded-growth

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

dstNodes had no Delete anywhere. Entries were Stored in the discover handler (line 652) and only ever Loaded on the relay path (line 1135). It appeared in no sweep — reapStaleNodes covers nodes, punchRL, relayRL, discoverRL, gossip peers and nodePubKeys, but never this one.

The node id is read straight off the wire (binary.BigEndian.Uint32(data[0:4])) in an unauthenticated datagram, so the key space is the full uint32 range and an attacker picks it freely. Steady, unbounded growth — on a process that runs embedded in the production registry.

Fix

Same treatment as nodePubKeys directly above it: an entry is only meaningful while the node is still known, so drop it once the node has aged out of s.nodes. Mirrors the existing pattern exactly, in the sweep that already runs.

Build, vet and all beacon tests pass.

Found during a codebase-wide sweep for resource leaks.

🤖 Generated with Claude Code

teovl and others added 2 commits July 28, 2026 19:17
dstNodes had no Delete anywhere. Entries were Stored in the discover handler
(line 652) and only ever Loaded on the relay path (line 1135). It appeared in
no sweep — reapStaleNodes covers nodes, punchRL, relayRL, discoverRL, gossip
peers and nodePubKeys, but never this one.

The node id is read straight off the wire (binary.BigEndian.Uint32(data[0:4]))
in an unauthenticated datagram, so the key space is the full uint32 range and
an attacker picks it freely. Steady, unbounded growth on a process that runs
embedded in the production registry.

Same treatment as nodePubKeys directly above: an entry is only meaningful
while the node is still known, so drop it once the node has aged out of
s.nodes. Mirrors the existing pattern exactly, in the sweep that already runs.

Found during a codebase-wide sweep for resource leaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestRelayBudgetIsPerDatagramSource asserted a flat ceiling of
maxRelaysPerSourcePerSecond after firing 3x that many packets in a loop. That
assumed the whole burst fits inside one rate window.

It does on a fast dev box (~0.25s) and does not on CI. The budget is a
one-second window — allow() rolls it when nowNano-windowStart >= 1s — and
dispatchRelay reads the real clock, so a loaded runner spending 2.3s in the
loop legitimately opens three windows and passes ~3x the per-window budget.
The test has been failing on main since it landed (e.g. run 30205771783,
2026-07-26, "enqueued 2344 ... budget is 1000"), so main has been red and
every branch inherited it.

The property under test is that rotating the SENDER ID does not multiply the
per-source budget — not how fast the machine is. So the ceiling is now scaled
by the number of windows the loop actually spanned, and a second assertion
pins the real invariant independently of timing: not every attempt may be
enqueued, which is exactly what would happen if per-source keying regressed.

Verified passing repeatedly, including with -cpu=1 to approximate a
contended runner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@TeoSlayer
TeoSlayer merged commit 9df92d1 into main Jul 29, 2026
5 checks passed
@TeoSlayer
TeoSlayer deleted the fix/dstnodes-unbounded-growth branch July 29, 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.

2 participants