ssh: reject unexpected message types on established channels

ch.msg is only read while the channel open or a channel request with a
reply is pending, so anything the default arm of channel.handlePacket
delivered to it was never consumed. The blocking send there let a
misbehaving peer fill the buffer with well-formed but unexpected message
types carrying a valid channel id and stall the mux read loop,
deadlocking the whole connection.

No conforming peer sends such messages during the connection protocol.
Treat them as a protocol error and tear the connection down, as
handleUnknownChannelPacket already does for the same messages when the
channel id is not in use.

Fixes CVE-2026-56855
Fixes golang/go#81317

Change-Id: I87420dfe68fcb62a17df4b47dc5ffb6ccd72ba26
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826524
Reviewed-by: Roland Shoemaker <roland@golang.org>
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: Nicholas Husin <husin@google.com>
This commit is contained in:
Nicola Murino
2026-09-02 11:02:47 -07:00
committed by Gopher Robot
parent a6cdac6084
commit 86efde54dc
2 changed files with 52 additions and 1 deletions
+2 -1
View File
@@ -519,7 +519,8 @@ func (ch *channel) handlePacket(packet []byte) error {
default:
}
default:
ch.msg <- msg
// No other message type is expected on an established channel.
return fmt.Errorf("ssh: unexpected message type %d on channel %d", packet[0], ch.localId)
}
return nil
}
+50
View File
@@ -5,9 +5,11 @@
package ssh
import (
"encoding/binary"
"errors"
"fmt"
"io"
"strings"
"sync"
"testing"
"time"
@@ -1360,6 +1362,54 @@ func TestChannelRejectedInboundDropsTraffic(t *testing.T) {
}
}
func TestChannelBogusMessageTearsDownMux(t *testing.T) {
// A message that is well-formed on the wire but is not a type expected on
// an established channel is a protocol error that tears the mux down.
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
serverRes := make(chan *channel, 1)
go func() {
newCh, ok := <-serverMux.incomingChannels
if !ok {
close(serverRes)
return
}
c, _, err := newCh.Accept()
if err != nil {
close(serverRes)
return
}
serverRes <- c.(*channel)
}()
if _, err := clientMux.openChannel("chan", nil); err != nil {
t.Fatalf("openChannel: %v", err)
}
serverCh := <-serverRes
if serverCh == nil {
t.Fatal("server did not accept channel")
}
// Craft a packet that the client mux routes to clientCh (bytes 1:5 hold the
// peer's channel id) and that decode accepts, but whose type is unexpected
// on a channel. The service-string length equals the channel id, so the
// same four bytes serve both as the routing id and as the string length.
id := serverCh.remoteId
packet := []byte{msgServiceRequest, 0, 0, 0, 0}
binary.BigEndian.PutUint32(packet[1:], id)
packet = append(packet, make([]byte, id)...)
if err := serverMux.conn.writePacket(packet); err != nil {
t.Fatalf("writePacket: %v", err)
}
// The mux loop exits with the protocol error.
if err := clientMux.Wait(); err == nil || !strings.Contains(err.Error(), "unexpected message type") {
t.Fatalf("mux error = %v, want unexpected message type", err)
}
}
func TestChannelCloseIdempotent(t *testing.T) {
a, b := memPipe()
defer a.Close()