Skip to content

Commit 3c7c869

Browse files
drakkanthatnealpatel
authored andcommitted
ssh: fix deadlock on unexpected channel responses
Previously, channel.handlePacket sent channelRequestSuccess and channelRequestFailure messages to ch.msg unconditionally via the default arm of its type switch. Because ch.msg is a bounded buffer (chanSize), a peer that sends a burst of unsolicited channel request responses for an open, idle channel fills the buffer and blocks the mux read loop on the next send. That stalls all packet processing on the connection, and because readLoop then backs up on t.incoming, closing the underlying net.Conn does not unblock either goroutine: user code observes Close() returning promptly while Wait() hangs and the mux, readLoop, and kexLoop goroutines leak permanently. This change mirrors the fix for the mux-level SendRequest path: a sentRequestPending atomic gate is set while a SendRequest with WantReply is in flight, handlePacket drops responses when the gate is closed, and uses a non-blocking send otherwise. SendRequest drains any spurious response that slipped through before discarding it, so the caller always observes the reply to its own request. This aligns with OpenSSH, which silently ignores channel confirm messages that do not match a pending request. Fixes golang/go#79564 Fixes CVE-2026-39830 Change-Id: I15e2add4bf7876bb0c6f921f8b57203d97e83f47 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781664 LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Neal Patel <nealpatel@google.com> Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-by: Roland Shoemaker <roland@golang.org>
1 parent 533fb3f commit 3c7c869

2 files changed

Lines changed: 268 additions & 0 deletions

File tree

ssh/channel.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"log"
1313
"sync"
14+
"sync/atomic"
1415
)
1516

1617
const (
@@ -183,6 +184,12 @@ type channel struct {
183184
// with WantReply=true outstanding. This lock is held by a
184185
// goroutine that has such an outgoing request pending.
185186
sentRequestMu sync.Mutex
187+
// sentRequestPending is set to true while a SendRequest call with
188+
// WantReply=true is in flight. handlePacket uses it as a gate: responses
189+
// arriving while no request is pending are dropped to prevent a
190+
// misbehaving peer from stalling the mux read loop by filling ch.msg
191+
// with unsolicited channelRequestSuccess/Failure messages.
192+
sentRequestPending atomic.Bool
186193

187194
incomingRequests chan *Request
188195

@@ -466,6 +473,18 @@ func (ch *channel) handlePacket(packet []byte) error {
466473
}
467474

468475
ch.incomingRequests <- &req
476+
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
477+
// Drop responses that arrive when no SendRequest is waiting, to
478+
// prevent a malicious peer from filling ch.msg and stalling the
479+
// mux read loop. The non-blocking send additionally protects the
480+
// loop if a well-behaved caller is slow to read.
481+
if !ch.sentRequestPending.Load() {
482+
return nil
483+
}
484+
select {
485+
case ch.msg <- msg:
486+
default:
487+
}
469488
default:
470489
ch.msg <- msg
471490
}
@@ -602,6 +621,24 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
602621
if wantReply {
603622
ch.sentRequestMu.Lock()
604623
defer ch.sentRequestMu.Unlock()
624+
625+
// Open the gate so that responses arriving while this request is in
626+
// flight are allowed to reach ch.msg. Responses arriving while no
627+
// request is pending are dropped by handlePacket.
628+
ch.sentRequestPending.Store(true)
629+
defer ch.sentRequestPending.Store(false)
630+
631+
// Drain any spurious responses that may have been buffered. This
632+
// prevents a previously buffered unexpected response from being
633+
// consumed instead of the actual response for this request.
634+
drain:
635+
for {
636+
select {
637+
case <-ch.msg:
638+
default:
639+
break drain
640+
}
641+
}
605642
}
606643

607644
msg := channelRequestMsg{

ssh/mux_test.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,3 +1135,234 @@ func TestMuxGlobalResponseAcceptedWhilePending(t *testing.T) {
11351135
clientMux.Close()
11361136
<-serverDone
11371137
}
1138+
1139+
func TestChannelUnexpectedResponsesDiscarded(t *testing.T) {
1140+
// A malicious peer that spams channelRequestSuccess/Failure messages
1141+
// for an open, idle channel must not be able to stall the mux read
1142+
// loop by filling ch.msg. After the flood, the channel must still be
1143+
// usable: a subsequent legitimate SendRequest receives its reply.
1144+
clientMux, serverMux := muxPair()
1145+
defer serverMux.Close()
1146+
defer clientMux.Close()
1147+
1148+
serverRes := make(chan *channel, 1)
1149+
go func() {
1150+
newCh, ok := <-serverMux.incomingChannels
1151+
if !ok {
1152+
close(serverRes)
1153+
return
1154+
}
1155+
c, _, err := newCh.Accept()
1156+
if err != nil {
1157+
close(serverRes)
1158+
return
1159+
}
1160+
serverRes <- c.(*channel)
1161+
}()
1162+
1163+
clientCh, err := clientMux.openChannel("chan", nil)
1164+
if err != nil {
1165+
t.Fatalf("openChannel: %v", err)
1166+
}
1167+
serverCh := <-serverRes
1168+
if serverCh == nil {
1169+
t.Fatal("server did not accept channel")
1170+
}
1171+
1172+
// Spam many unsolicited success/failure responses. More than chanSize
1173+
// to ensure ch.msg would overflow without the pending-gate.
1174+
const spam = chanSize * 4
1175+
done := make(chan error, 1)
1176+
go func() {
1177+
for i := range spam {
1178+
if err := serverCh.ackRequest(i%2 == 0); err != nil {
1179+
done <- fmt.Errorf("ackRequest %d: %w", i, err)
1180+
return
1181+
}
1182+
}
1183+
// Echo any legitimate request back.
1184+
for req := range serverCh.incomingRequests {
1185+
if req.WantReply {
1186+
if err := req.Reply(true, append([]byte("reply:"), req.Payload...)); err != nil {
1187+
done <- fmt.Errorf("reply: %w", err)
1188+
return
1189+
}
1190+
}
1191+
}
1192+
done <- nil
1193+
}()
1194+
1195+
// If the flood had wedged the mux loop, this SendRequest would never
1196+
// receive a reply.
1197+
ok, err := clientCh.SendRequest("ping", true, []byte("hello"))
1198+
if err != nil {
1199+
t.Fatalf("SendRequest: %v", err)
1200+
}
1201+
if !ok {
1202+
t.Fatal("expected success reply")
1203+
}
1204+
1205+
// Clean up so the server goroutine can exit.
1206+
clientCh.Close()
1207+
serverCh.Close()
1208+
if err := <-done; err != nil {
1209+
if !errors.Is(err, io.EOF) {
1210+
t.Fatal(err)
1211+
}
1212+
}
1213+
}
1214+
1215+
func TestChannelConcurrentRequests(t *testing.T) {
1216+
writer, reader, mux := channelPair(t)
1217+
defer writer.Close()
1218+
defer reader.Close()
1219+
defer mux.Close()
1220+
1221+
serverDone := make(chan struct{})
1222+
go func() {
1223+
defer close(serverDone)
1224+
for req := range writer.incomingRequests {
1225+
if req.WantReply {
1226+
req.Reply(true, append([]byte("reply:"), req.Payload...))
1227+
}
1228+
}
1229+
}()
1230+
1231+
const numRequests = 50
1232+
var wg sync.WaitGroup
1233+
wg.Add(numRequests)
1234+
errCh := make(chan error, numRequests)
1235+
1236+
for i := 0; i < numRequests; i++ {
1237+
go func(id int) {
1238+
defer wg.Done()
1239+
payload := []byte(fmt.Sprintf("req-%d", id))
1240+
ok, err := reader.SendRequest("echo", true, payload)
1241+
if err != nil {
1242+
errCh <- fmt.Errorf("req %d: %v", id, err)
1243+
return
1244+
}
1245+
if !ok {
1246+
errCh <- fmt.Errorf("req %d: expected success", id)
1247+
}
1248+
}(i)
1249+
}
1250+
1251+
wg.Wait()
1252+
close(errCh)
1253+
1254+
for err := range errCh {
1255+
if err != nil {
1256+
t.Fatal(err)
1257+
}
1258+
}
1259+
1260+
reader.Close()
1261+
writer.Close()
1262+
<-serverDone
1263+
}
1264+
1265+
func TestChannelResponseDroppedWhenIdle(t *testing.T) {
1266+
// A spurious response arriving while no SendRequest is pending must
1267+
// be dropped rather than buffered in ch.msg.
1268+
writer, reader, mux := channelPair(t)
1269+
defer writer.Close()
1270+
defer reader.Close()
1271+
defer mux.Close()
1272+
1273+
// Server sends an unsolicited reply, then a request so we can
1274+
// synchronise: once the client observes the request, the mux loop has
1275+
// necessarily processed (and dropped) the prior spurious reply.
1276+
errCh := make(chan error, 1)
1277+
go func() {
1278+
if err := writer.ackRequest(true); err != nil {
1279+
errCh <- err
1280+
return
1281+
}
1282+
if _, err := writer.SendRequest("sync", false, nil); err != nil {
1283+
errCh <- err
1284+
return
1285+
}
1286+
errCh <- nil
1287+
}()
1288+
1289+
req := <-reader.incomingRequests
1290+
if req.Type != "sync" {
1291+
t.Fatalf("unexpected request type %q", req.Type)
1292+
}
1293+
1294+
if n := len(reader.msg); n != 0 {
1295+
t.Fatalf("ch.msg should be empty after idle drop, has %d entries", n)
1296+
}
1297+
1298+
if err := <-errCh; err != nil {
1299+
t.Fatal(err)
1300+
}
1301+
}
1302+
1303+
func TestChannelStaleResponseDrained(t *testing.T) {
1304+
// Simulate a stale response sitting in ch.msg (e.g. a response that
1305+
// slipped through the pending-gate on a prior SendRequest that exited
1306+
// without consuming it). The drain step in the next SendRequest must
1307+
// discard it so the caller receives the correct reply.
1308+
writer, reader, mux := channelPair(t)
1309+
defer writer.Close()
1310+
defer reader.Close()
1311+
defer mux.Close()
1312+
1313+
reader.msg <- &channelRequestSuccessMsg{PeersID: reader.remoteId}
1314+
1315+
serverDone := make(chan struct{})
1316+
go func() {
1317+
defer close(serverDone)
1318+
for req := range writer.incomingRequests {
1319+
if req.WantReply {
1320+
req.Reply(false, append([]byte("nack:"), req.Payload...))
1321+
}
1322+
}
1323+
}()
1324+
1325+
ok, err := reader.SendRequest("test", true, []byte("hello"))
1326+
if err != nil {
1327+
t.Fatalf("SendRequest: %v", err)
1328+
}
1329+
// If the stale success had been consumed, ok would be true.
1330+
if ok {
1331+
t.Fatal("got stale success response; drain did not remove it")
1332+
}
1333+
1334+
reader.Close()
1335+
writer.Close()
1336+
<-serverDone
1337+
}
1338+
1339+
func TestChannelResponseAcceptedWhilePending(t *testing.T) {
1340+
// Positive control: when a SendRequest is actually pending, the
1341+
// response must be delivered (the gate is open).
1342+
writer, reader, mux := channelPair(t)
1343+
defer writer.Close()
1344+
defer reader.Close()
1345+
defer mux.Close()
1346+
1347+
serverDone := make(chan struct{})
1348+
go func() {
1349+
defer close(serverDone)
1350+
for req := range writer.incomingRequests {
1351+
if req.WantReply {
1352+
req.Reply(true, nil)
1353+
}
1354+
}
1355+
}()
1356+
1357+
ok, err := reader.SendRequest("ping", true, nil)
1358+
if err != nil {
1359+
t.Fatalf("SendRequest: %v", err)
1360+
}
1361+
if !ok {
1362+
t.Fatal("expected success")
1363+
}
1364+
1365+
reader.Close()
1366+
writer.Close()
1367+
<-serverDone
1368+
}

0 commit comments

Comments
 (0)