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>
diff --git a/ssh/channel.go b/ssh/channel.go
index 0d9dac9..d6010fd 100644
--- a/ssh/channel.go
+++ b/ssh/channel.go
@@ -519,7 +519,8 @@
 		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
 }
diff --git a/ssh/mux_test.go b/ssh/mux_test.go
index b844082..e23fd0e 100644
--- a/ssh/mux_test.go
+++ b/ssh/mux_test.go
@@ -5,9 +5,11 @@
 package ssh
 
 import (
+	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
+	"strings"
 	"sync"
 	"testing"
 	"time"
@@ -1360,6 +1362,54 @@
 	}
 }
 
+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()