Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions SPECS/moby-engine/CVE-2026-37236.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
From 72123cd4f32545f6e1376873f412dcdcbcf29acc Mon Sep 17 00:00:00 2001
From: Andrew Z Allen <me@andrewzallen.com>
Date: Fri, 6 Mar 2026 23:50:33 -0700
Subject: [PATCH] Add WithDisableHTTPMethodOverride ServeMux option (#6447)

Add a new server option that disables the X-HTTP-Method-Override header
handling independently of the path length fallback. This allows users to
prevent POST requests from having their method overridden via the header
while still allowing the POST-to-GET fallback for form-urlencoded requests.

This work is inspired by the security researcher Mariusz Maik. Thank you for
your hard work and tireless bughunting!

[Azure Linux backport note] This addresses CVE-2026-37236 / GHSA-6gx8-r37x-4vw8
(X-HTTP-Method-Override access-control bypass in
github.com/grpc-ecosystem/grpc-gateway/v2). moby v25.0.3 vendors grpc-gateway
v2.16.0, so the upstream hunks did not apply cleanly and were rebased onto the
v2.16.0 layout:
* The ServeMux struct in v2.16.0 ends with disablePathLengthFallback followed
by unescapingMode (it has no writeContentLength/disableChunkedEncoding
fields), so the new disableHTTPMethodOverride field is inserted immediately
before unescapingMode.
* v2.16.0 has no WithWriteContentLength option (the upstream anchor), so the
new WithDisableHTTPMethodOverride option is inserted right after
WithDisablePathLengthFallback instead.
* In v2.16.0 ServeHTTP assigns r.Method before calling r.ParseForm(); that
existing ordering is preserved on purpose (the unrelated upstream ParseForm
reordering commit 5d1f4c1c62ec is NOT backported). Only the guard condition
gains the new "!s.disableHTTPMethodOverride" term.

The fix is OPT-IN and does not change default behaviour: X-HTTP-Method-Override
handling is disabled only when the new WithDisableHTTPMethodOverride()
ServeMuxOption is passed to NewServeMux, exactly matching upstream v2.29.0. This
mirrors upstream rather than flipping the default (grpc-gateway is vendored dead
code in dockerd, which is an OTLP client and never constructs a ServeMux). The
upstream docs/ and runtime/mux_test.go hunks are omitted because docs and
_test.go files are not vendored; vendor/modules.txt, vendor.mod and vendor.sum
are intentionally left at v2.16.0.

Upstream Patch Reference: https://github.com/grpc-ecosystem/grpc-gateway/commit/72123cd4f32545f6e1376873f412dcdcbcf29acc.patch
---
vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/mux.go | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/mux.go b/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/mux.go
index f451cb4..284eff5 100644
--- a/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/mux.go
+++ b/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/mux.go
@@ -62,6 +62,7 @@ type ServeMux struct {
streamErrorHandler StreamErrorHandlerFunc
routingErrorHandler RoutingErrorHandlerFunc
disablePathLengthFallback bool
+ disableHTTPMethodOverride bool
unescapingMode UnescapingMode
}

@@ -204,6 +205,19 @@ func WithDisablePathLengthFallback() ServeMuxOption {
}
}

+// WithDisableHTTPMethodOverride returns a ServeMuxOption that disables the
+// X-HTTP-Method-Override header handling.
+//
+// When this option is used, the mux will no longer allow POST requests with
+// the X-HTTP-Method-Override header to override the HTTP method. The path
+// length fallback (POST with application/x-www-form-urlencoded falling back
+// to a matching GET handler) is not affected by this option.
+func WithDisableHTTPMethodOverride() ServeMuxOption {
+ return func(serveMux *ServeMux) {
+ serveMux.disableHTTPMethodOverride = true
+ }
+}
+
// WithHealthEndpointAt returns a ServeMuxOption that will add an endpoint to the created ServeMux at the path specified by endpointPath.
// When called the handler will forward the request to the upstream grpc service health check (defined in the
// gRPC Health Checking Protocol).
@@ -320,7 +334,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path = r.URL.RawPath
}

- if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) {
+ if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && !s.disableHTTPMethodOverride && s.isPathLengthFallback(r) {
r.Method = strings.ToUpper(override)
if err := r.ParseForm(); err != nil {
_, outboundMarshaler := MarshalerForRequest(s, r)
--
2.45.4
177 changes: 177 additions & 0 deletions SPECS/moby-engine/CVE-2026-56855.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
From 86efde54dc7069251a8b007026c500d28e4239ce Mon Sep 17 00:00:00 2001
From: Nicola Murino <nicola.murino@gmail.com>
Date: Sat, 13 Jun 2026 11:48:20 +0200
Subject: [PATCH] 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 backport squashes two required prerequisite commits into the same
patch so that CVE-2026-56855's one-line change neither regresses reply
handling nor ships a known busy-spin:

* Prerequisite 1: golang/crypto 3c7c86938f4541c333d506f719388d9c42d4763d
"ssh: fix deadlock on unexpected channel responses" (the channel half
of CVE-2026-39830; the spec previously ported only the mux/global-request
half in CVE-2026-39830.patch). It adds the sentRequestPending atomic.Bool
gate, an explicit "case *channelRequestSuccessMsg, *channelRequestFailureMsg:"
arm in handlePacket (non-blocking send behind the pending gate), and the
gate-open/drain logic in SendRequest. In v0.17.0,
SendRequest(wantReply=true) receives its reply through the default arm of
handlePacket, so converting default into an error return WITHOUT this
prerequisite would make every exec/shell/pty-req/subsystem request with
WantReply=true tear the connection down with "ssh: unexpected message
type 99". The prerequisite adds the explicit success/failure arm first,
so those replies still reach ch.msg and SendRequest still works.

* Prerequisite 2: golang/crypto e3e62d9601ec6fa737c081aead768f525f919802
"ssh: fix spinloop in channel SendRequest drain on closed channel"
(golang/go#79658). The drain loop added by prerequisite 1 receives from
ch.msg WITHOUT the comma-ok flag. Once channel.close() closes ch.msg
(which mux.loop() does for every channel via dropAll() when the
connection is torn down), the receive succeeds immediately and forever,
the default arm is never taken, and the loop spins at 100% CPU and never
returns. This is reached by every SendRequest(wantReply=true) -
Session.Start/Shell/Setenv/Signal/RequestPty/WindowChange/Subsystem - on
a dropped connection; pre-patch that call fell through to sendMessage and
returned io.EOF promptly. The comma-ok idiom detects the closed channel
and breaks out of the drain loop.

The SAME unguarded drain loop was introduced into mux.SendRequest (over
m.globalResponses) by the already-shipped CVE-2026-39830.patch, and
mux.loop() closes m.globalResponses on exit, so it has the identical
100%-CPU spinloop on teardown, reached by
client.SendRequest(..., wantReply=true, ...). e3e62d9's published diff
carries only the ssh/channel.go hunk, but canonical golang.org/x/crypto
now guards mux.SendRequest with the same comma-ok idiom; this backport
applies it to ssh/mux.go as well. Shipping the channel.go fix alone would
leave a known connection-teardown DoS (busy-spin) in the mux path.

* CVE-2026-56855 (86efde54dc7069251a8b007026c500d28e4239ce): replaces the
remaining "default: ch.msg <- msg" blocking send with a protocol error,
so a peer can no longer stall the mux read loop with well-formed but
unexpected message types.

Squashing multiple upstream commits into one CVE patch file follows the
existing practice in this spec (see CVE-2026-17106.patch).

Backport notes (Azure Linux, moby-engine 25.0.3, vendored golang.org/x/crypto v0.17.0):
* The "sync/atomic" import that sentRequestPending needs is already added
by CVE-2026-78662.patch (Patch30, applied first), so this patch does not
re-add it.
* Dropped every _test.go hunk from all three upstream commits: those test
files (ssh/mux_test.go) are not vendored.

Upstream Patch Reference: https://github.com/golang/crypto/commit/86efde54dc7069251a8b007026c500d28e4239ce.patch
Prerequisite Patch Reference: https://github.com/golang/crypto/commit/3c7c86938f4541c333d506f719388d9c42d4763d.patch
Prerequisite Patch Reference: https://github.com/golang/crypto/commit/e3e62d9601ec6fa737c081aead768f525f919802.patch
---
vendor/golang.org/x/crypto/ssh/channel.go | 42 ++++++++++++++++++++++++++++++-
vendor/golang.org/x/crypto/ssh/mux.go | 5 +++-
2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/vendor/golang.org/x/crypto/ssh/channel.go b/vendor/golang.org/x/crypto/ssh/channel.go
index cb4ff2b..4403564 100644
--- a/vendor/golang.org/x/crypto/ssh/channel.go
+++ b/vendor/golang.org/x/crypto/ssh/channel.go
@@ -190,6 +190,12 @@ type channel struct {
// with WantReply=true outstanding. This lock is held by a
// goroutine that has such an outgoing request pending.
sentRequestMu sync.Mutex
+ // sentRequestPending is set to true while a SendRequest call with
+ // WantReply=true is in flight. handlePacket uses it as a gate: responses
+ // arriving while no request is pending are dropped to prevent a
+ // misbehaving peer from stalling the mux read loop by filling ch.msg
+ // with unsolicited channelRequestSuccess/Failure messages.
+ sentRequestPending atomic.Bool

incomingRequests chan *Request

@@ -483,8 +489,21 @@ func (ch *channel) handlePacket(packet []byte) error {
}

ch.incomingRequests <- &req
+ case *channelRequestSuccessMsg, *channelRequestFailureMsg:
+ // Drop responses that arrive when no SendRequest is waiting, to
+ // prevent a malicious peer from filling ch.msg and stalling the
+ // mux read loop. The non-blocking send additionally protects the
+ // loop if a well-behaved caller is slow to read.
+ if !ch.sentRequestPending.Load() {
+ return nil
+ }
+ select {
+ case ch.msg <- msg:
+ 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
}
@@ -620,6 +639,27 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
if wantReply {
ch.sentRequestMu.Lock()
defer ch.sentRequestMu.Unlock()
+
+ // Open the gate so that responses arriving while this request is in
+ // flight are allowed to reach ch.msg. Responses arriving while no
+ // request is pending are dropped by handlePacket.
+ ch.sentRequestPending.Store(true)
+ defer ch.sentRequestPending.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 := <-ch.msg:
+ if !ok {
+ break drain
+ }
+ default:
+ break drain
+ }
+ }
}

msg := channelRequestMsg{
diff --git a/vendor/golang.org/x/crypto/ssh/mux.go b/vendor/golang.org/x/crypto/ssh/mux.go
index 3bc4afb..5775881 100644
--- a/vendor/golang.org/x/crypto/ssh/mux.go
+++ b/vendor/golang.org/x/crypto/ssh/mux.go
@@ -155,7 +155,10 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
drain:
for {
select {
- case <-m.globalResponses:
+ case _, ok := <-m.globalResponses:
+ if !ok {
+ break drain
+ }
default:
break drain
}
--
2.45.4
109 changes: 109 additions & 0 deletions SPECS/moby-engine/CVE-2026-78662.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
From a6cdac60840750226b15617ac8858be44361b36b Mon Sep 17 00:00:00 2001
From: Nicola Murino <nicola.murino@gmail.com>
Date: Sat, 13 Jun 2026 11:54:07 +0200
Subject: [PATCH] ssh: drop traffic on undecided channels

A channel in the mux's chanList is not usable until it is established:
an outbound channel has no confirmed remote id until the peer's open
confirmation, and an inbound channel is not serviced by the application
until it is accepted. handlePacket processed any channel message on it,
so a misbehaving peer could flood channel requests and block the mux
read loop on the send to incomingRequests, deadlocking the connection,
or close an outbound channel before confirming it, making the victim
tear down a half-initialized channel and emit a close for remote id 0,
an unrelated channel of the peer.

No such packet can be legitimate: the peer learns an inbound channel's
local id only from the confirmation we have not sent yet, and on an
outbound channel RFC 4254 lets it answer the open request only with a
confirmation or a failure.

Add an established flag, set when the channel becomes usable: for an
outbound channel when the open response is received, for an inbound
channel by Accept before the confirmation is sent. Until then
handlePacket drops every packet other than the open response. The flag
is separate from decided, which Reject also sets: a rejected channel is
decided but must never carry traffic.

Fixes CVE-2026-78662
Fixes golang/go#81316

Change-Id: Ib0983bb216a49808a2db1f4a4d92ee9fe38a3c51
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/826504
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>

Backport notes (Azure Linux, moby-engine 25.0.3, vendored golang.org/x/crypto v0.17.0):
* Added "sync/atomic" to the ssh/channel.go import block. Upstream's
channel.go already imported sync/atomic (for an unrelated
sentRequestPending field that does not exist in v0.17.0), so the
upstream commit carried no import hunk; without this addition the
build fails with "channel.go: undefined: atomic".
* Dropped the upstream ssh/mux_test.go hunk: that test file is not
vendored, and it references unvendored helpers (memPipe, muxPair,
chanSize) and Go 1.22 range-over-int syntax.

Upstream Patch Reference: https://github.com/golang/crypto/commit/a6cdac60840750226b15617ac8858be44361b36b.patch
---
vendor/golang.org/x/crypto/ssh/channel.go | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)

diff --git a/vendor/golang.org/x/crypto/ssh/channel.go b/vendor/golang.org/x/crypto/ssh/channel.go
index 77bac19..cb4ff2b 100644
--- a/vendor/golang.org/x/crypto/ssh/channel.go
+++ b/vendor/golang.org/x/crypto/ssh/channel.go
@@ -11,6 +11,7 @@ import (
"io"
"log"
"sync"
+ "sync/atomic"
)

const (
@@ -172,6 +173,12 @@ type channel struct {
// (for outbound channels) or received (for inbound channels).
decided bool

+ // established is set to true once the channel is open and may carry normal
+ // channel traffic: for an outbound channel when the peer's open
+ // confirmation is received, for an inbound channel when the local side
+ // accepts it. It is set and read from different goroutines.
+ established atomic.Bool
+
// direction contains either channelOutbound, for channels created
// locally, or channelInbound, for channels created by the peer.
direction channelDirection
@@ -410,10 +417,20 @@ func (ch *channel) responseMessageReceived() error {
return errors.New("ssh: duplicate response received for channel")
}
ch.decided = true
+ ch.established.Store(true)
return nil
}

func (ch *channel) handlePacket(packet []byte) error {
+ // Only the open response is expected before the channel is established.
+ if !ch.established.Load() {
+ switch packet[0] {
+ case msgChannelOpenConfirm, msgChannelOpenFailure:
+ default:
+ return nil
+ }
+ }
+
switch packet[0] {
case msgChannelData, msgChannelExtendedData:
return ch.handleData(packet)
@@ -518,6 +535,7 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) {
MaxPacketSize: ch.maxIncomingPayload,
}
ch.decided = true
+ ch.established.Store(true)
if err := ch.sendMessage(confirm); err != nil {
return nil, nil, err
}
--
2.45.4
Loading
Loading