CVE-2026-98595: udp: remove a disconnected socket from the 4-tuple hash table
In the Linux kernel, the following vulnerability has been resolved:
udp: remove a disconnected socket from the 4-tuple hash table
A UDP socket bound to a specific address and port keeps its entry in the 4-tuple hash table after it is disconnected:
sk binds to 127.0.0.1:21001 sk connects to 127.0.0.2:20001 // filed in the 4-tuple table sk disconnects, connect(AFUNSPEC) // still filed, peer now 0.0.0.0:0
udpdisconnect() takes a socket out of that table only as a side effect of ->rehash() or ->unhash(), and it skips ->rehash() when SOCKBINDADDRLOCK is set and ->unhash() when SOCKBINDPORTLOCK is set. commit 6996a2d2d0a6 ("udp: Unhash auto-bound connected sk from 4-tuple hash table when disconnected.") fixed the same end state for a wildcard-bound socket, by a path this one does not take.
The entry is counted whether or not anything hits it. hash4cnt on the hash2 slot stays raised for as long as the socket lives, so udphashash4() keeps sending every packet for that address and port through the 4-tuple lookup first.
On IPv6 it can also be hit. udpdisconnect() does not clear skv6daddr, so udpv6rehash() files the entry under the peer the socket was connected to with a zero dport, and inet6match() compares that same field: a datagram from the former peer with a zero source port matches, and source port zero is accepted on receive. On IPv4 the peer is cleared, so a match would need a zero source address as well, which the routing layer rejects as martian. The stale skv6daddr is a separate defect, not addressed here; removing the entry closes this path either way.
The entry can also be relocated. udpdisconnect() clears skbounddevif, so a subsequent SOBINDTODEVICE calls ->rehash(), and because the receive address is still specific udplibrehash() moves the entry instead of removing it, into the bucket that (rcvsaddr, num, 0, 0) hashes to -- a pure function of the address and port, so every socket reaching this state on one address and port collects in one bucket. The bucket cannot be chosen from outside, as udpehashfn() is seeded with a per-boot secret. This last one became reachable only with commit 644f9108f3a5 ("udp: Make rehash4 independent in udplibrehash()"), which moved the hash4 handling out of a branch a disconnected socket does not take; the stale entry itself dates from the commit in Fixes.
Take the socket out of the table before udpdisconnect() runs, while it still matches how it was filed. This also reaches the wildcard case ahead of udplibrehash()'s udpunhash4() branch, leaving that branch unreachable from udpdisconnect(); removing it belongs in net-next. udpdisconnect() and udpabort() are the only UDP entries into udpdisconnect(), which is shared with raw, ping and l2tp sockets that are not struct udpsock: pingprot.objsize is sizeof(struct inetsock), so udphashed4() on one would read past the allocation.