Free idle-peer IPs only when allPeers owns them - #28
Draft
saltzm wants to merge 1 commit into
Draft
Conversation
removeIdlePeers freed IPs from the WireGuard AllowedIPs snapshot even for peers no longer present in allPeers. When a /disconnect (cleanupPeer) and the GC loop process the same peer concurrently, this could free an IP that a racing /connect had already reallocated, corrupting IpAllocator state. Gate the free on the allPeers entry and free the tracked PeerIp instead. Co-Authored-By: matthew@modal.com <saltzm@gmail.com>
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
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.
Summary
While investigating the recurring
peer unexpectedly not found in allPeers - did /disconnect race with the periodic peer-GC loop?warning (~2.8k/wk in Datadog), I found the warning itself is benign —allPeersis correctly guarded bysrv.muand the losing caller correctly no-ops. But the same race exposes a real, if rare, correctness bug in the IP allocator, which this PR fixes.The bug: free-realloc-free in
removeIdlePeersTwo removal paths free IPs differently:
cleanupPeer(the/disconnectpath) freespeerInfo.PeerIpfromallPeers, and only when it actually removed the entry.removeIdlePeers(the GC loop) freed the IP taken from the WireGuardpeer.AllowedIPssnapshot, for every idle peer — including peers with noallPeersentry (theidle=true"no entry" branch, which is exactly the peer involved in the race that emits the warning).That opens a window where an in-use IP gets freed:
IpAllocator.Freeguards against a plain back-to-back double-free (it checks membership before deleting/decrementing), but the reallocation in step 2 defeats that guard:Xis dropped fromallocatedwhile a live peer holds it. Impact:Xcan later be handed to a second peer → duplicate tunnel IP → mis-SNAT / routing breakage.allocated_ipsgauge drifts below the true count.Low probability (needs a reconnect to land in the window) but a genuine correctness issue, not just log noise.
The fix
Gate the IP free on
allPeersownership and free the trackedPeerIp, so both removal paths free each IP exactly once — by whoever wins theallPeersdelete undersrv.mu:Normal peers are unaffected —
PeerIpequals the value previously read fromAllowedIPs[0]. Peers still get removed from WireGuard regardless; only the allocator free is now gated. The warning log is intentionally left in place as an accurate race indicator.Not addressed here (follow-up)
connectHandlerreadsallPeersunder one lock, releases it, thenAllocate()s and re-inserts under a second lock. A reconnect of an idle peer racing GC can leaveallPeers/WireGuard holding an IP the allocator has freed. Fixing that means doing check + allocate + insert in a single critical section — rarer and riskier, so left out of this minimal change.Testing
go build ./...,go vet ./...,gofmt -l lib/server.go(clean),go test ./...all pass.Link to Devin session: https://modal.devinenterprise.com/sessions/6820fff430bc46799775cbc191010b7d
Requested by: @saltzm