From d66e1f8827194fbc2354732aae864934272d6d33 Mon Sep 17 00:00:00 2001 From: yiraeChristineKim Date: Tue, 30 Jun 2026 21:53:49 -0400 Subject: [PATCH] ssh: fix deadlock on unexpected global responses (CVE-2026-39830) Backport of upstream fix golang/crypto@4e7a7384ecbc onto v0.39.0 for Go 1.23 compatibility (release-4.19). The go.mod go directive remains at 1.23.0; no go 1.25 language features are used. Previously, handleGlobalPacket blocked on sending to globalResponses (buffer size 1), allowing a malicious SSH peer to stall the connection read loop permanently. Fix: replace the blocking send with a conditional non-blocking send gated on globalSentPending. A drain loop in SendRequest discards any stale buffered responses before sending a new request. CVE: CVE-2026-39830 GO: GO-2026-5017 Upstream: https://github.com/golang/crypto/commit/4e7a7384ecbc Co-authored-by: Cursor --- ssh/mux.go | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/ssh/mux.go b/ssh/mux.go index d2d24c635d..5775881c6a 100644 --- a/ssh/mux.go +++ b/ssh/mux.go @@ -91,9 +91,10 @@ type mux struct { incomingChannels chan NewChannel - globalSentMu sync.Mutex - globalResponses chan interface{} - incomingRequests chan *Request + globalSentMu sync.Mutex + globalSentPending atomic.Bool + globalResponses chan interface{} + incomingRequests chan *Request errCond *sync.Cond err error @@ -141,6 +142,27 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, [] if wantReply { m.globalSentMu.Lock() defer m.globalSentMu.Unlock() + + // Open the gate so that responses arriving while this request is in + // flight are allowed to reach globalResponses. Any response arriving + // while no request is pending is dropped by handleGlobalPacket. + m.globalSentPending.Store(true) + defer m.globalSentPending.Store(false) + + // Drain any spurious responses that may have been buffered. This prevents + // a previously buffered unexpected response from being consumed instead + // of the actual response for this request. + drain: + for { + select { + case _, ok := <-m.globalResponses: + if !ok { + break drain + } + default: + break drain + } + } } if err := m.sendMessage(globalRequestMsg{ @@ -267,7 +289,16 @@ func (m *mux) handleGlobalPacket(packet []byte) error { mux: m, } case *globalRequestSuccessMsg, *globalRequestFailureMsg: - m.globalResponses <- msg + // Drop responses that arrive when no SendRequest is waiting, to + // prevent a malicious peer from staging responses for a future + // caller. + if !m.globalSentPending.Load() { + return nil + } + select { + case m.globalResponses <- msg: + default: + } default: panic(fmt.Sprintf("not a global message %#v", msg)) }