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
104 changes: 104 additions & 0 deletions SPECS/cert-manager/CVE-2026-39830.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
From 325ed0ec1bcf9da9e88459fbe5ca4a8933452fe1 Mon Sep 17 00:00:00 2001
From: Nicola Murino <nicola.murino@gmail.com>
Date: Sun, 25 Jan 2026 19:08:01 +0100
Subject: [PATCH] ssh: fix deadlock on unexpected global responses

Previously, the mux implementation handled global request responses by
blocking until the response could be sent to the globalResponses channel.
Since this channel has a buffer size of 1, unsolicited responses from a
server (or responses arriving after a timeout) would fill the buffer.
Subsequent unsolicited responses would block handleGlobalPacket, stalling
the entire connection's read loop and causing a denial of service.

This change modifies handleGlobalPacket to use a non-blocking send. If
no goroutine is waiting for a response (or the buffer is full), the
message is dropped. This aligns with OpenSSH behavior, which ignores
unexpected global responses.

Additionally, SendRequest now drains the globalResponses channel after
acquiring the mutex but before sending the request. This ensures that
any stale responses or "spam" buffered just before the lock was acquired
are discarded, preventing race conditions where a legitimate request
might otherwise consume an unrelated response.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79564
Fixes CVE-2026-39830

Change-Id: Ia0c46355203d557eadcd432c10b87c8a044e1089
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781640
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/TheDegenerateDev5150/crypto/commit/4e7a7384ecbc8d519f6f4c11b36fa9d761fc8946.patch
---
.../vendor/golang.org/x/crypto/ssh/mux.go | 36 ++++++++++++++++---
1 file changed, 32 insertions(+), 4 deletions(-)

diff --git a/cmd/controller/vendor/golang.org/x/crypto/ssh/mux.go b/cmd/controller/vendor/golang.org/x/crypto/ssh/mux.go
index d2d24c6..3bc4afb 100644
--- a/cmd/controller/vendor/golang.org/x/crypto/ssh/mux.go
+++ b/cmd/controller/vendor/golang.org/x/crypto/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,24 @@ 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 <-m.globalResponses:
+ default:
+ break drain
+ }
+ }
}

if err := m.sendMessage(globalRequestMsg{
@@ -267,7 +286,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))
}
--
2.45.4

222 changes: 222 additions & 0 deletions SPECS/cert-manager/CVE-2026-39834.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
From 14cfd9fee445fa3190fdda36bf3975dee1bed8e1 Mon Sep 17 00:00:00 2001
From: Nicola Murino <nicola.murino@gmail.com>
Date: Sun, 14 Dec 2025 15:32:31 +0100
Subject: [PATCH] ssh: fix infinite loop on large channel writes due to integer
overflow

The internal 'min' helper function in channel.go incorrectly cast the
input data length (int) to uint32 before comparing it with the
maximum packet size. On 64-bit systems, if the data length is a
multiple of 2^32 (approx. 4GB), this cast results in 0.

Consequently, the function returns 0, causing the WriteExtended loop
to spin indefinitely because it attempts to reserve 0 bytes while
the remaining data length is still positive.

This change renames the helper to 'minPayloadSize' to avoid confusion
with the Go 1.21 built-in 'min' and updates the logic to use int64
for comparisons, preventing truncation and the resulting infinite loop.

This issue was found during a security audit by NCC Group Cryptography
Services, sponsored by Teleport.

Fixes golang/go#79567
Fixes CVE-2026-39834

Change-Id: Id5bf81d9f06c7042452acffe1c76580ff878665e
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/781663
Reviewed-by: Neal Patel <nealpatel@google.com>
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>
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/TheDegenerateDev5150/crypto/commit/e052873987615dc96fe67607a9a6adb76311344f.patch
---
.../vendor/golang.org/x/crypto/ssh/channel.go | 16 +-
ssh/channel_test.go | 140 ++++++++++++++++++
2 files changed, 151 insertions(+), 5 deletions(-)
create mode 100644 ssh/channel_test.go

diff --git a/cmd/controller/vendor/golang.org/x/crypto/ssh/channel.go b/cmd/controller/vendor/golang.org/x/crypto/ssh/channel.go
index cc0bb7a..3967b65 100644
--- a/cmd/controller/vendor/golang.org/x/crypto/ssh/channel.go
+++ b/cmd/controller/vendor/golang.org/x/crypto/ssh/channel.go
@@ -131,11 +131,17 @@ func (r RejectionReason) String() string {
return fmt.Sprintf("unknown reason %d", int(r))
}

-func min(a uint32, b int) uint32 {
- if a < uint32(b) {
- return a
+// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
+// to compute the size of the next channel data packet from the remaining
+// payload. The comparison is done in int64 because length is an int — on
+// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
+// cast would silently truncate to 0 at every multiple of 2^32, causing
+// WriteExtended's loop to spin without making progress.
+func minPayloadSize(limit uint32, length int) uint32 {
+ if int64(length) > int64(limit) {
+ return limit
}
- return uint32(b)
+ return uint32(length)
}

type channelDirection uint8
@@ -251,7 +257,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
ch.writeMu.Unlock()

for len(data) > 0 {
- space := min(ch.maxRemotePayload, len(data))
+ space := minPayloadSize(ch.maxRemotePayload, len(data))
if space, err = ch.remoteWin.reserve(space); err != nil {
return n, err
}
diff --git a/ssh/channel_test.go b/ssh/channel_test.go
new file mode 100644
index 0000000..2266863
--- /dev/null
+++ b/ssh/channel_test.go
@@ -0,0 +1,140 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+ "math/bits"
+ "testing"
+ "time"
+ "unsafe"
+)
+
+func TestMinPayloadSize(t *testing.T) {
+ // 4 GiB (2^32). Declared as a var (not a const) so that int(bigPayload)
+ // is a runtime conversion: a constant conversion would fail to compile
+ // on 32-bit platforms with "constant 4294967296 overflows int". On
+ // 32-bit the value truncates to 0 at runtime, but the is64Bit cases
+ // that reference it are skipped by the runtime check below.
+ var bigPayload int64 = 1 << 32
+
+ tests := []struct {
+ name string
+ maxPayload uint32
+ dataLen int
+ want uint32
+ is64Bit bool // Flag to run only on 64-bit architectures
+ }{
+ {
+ name: "Normal Case - Data fits in payload",
+ maxPayload: 32768,
+ dataLen: 1000,
+ want: 1000,
+ },
+ {
+ name: "Normal Case - Data larger than payload",
+ maxPayload: 32768,
+ dataLen: 50000,
+ want: 32768,
+ },
+ {
+ name: "Boundary Case - Data zero",
+ maxPayload: 32768,
+ dataLen: 0,
+ want: 0,
+ },
+ {
+ name: "Overflow Case - Data is exactly 4GB (1<<32)",
+ maxPayload: 32768,
+ dataLen: int(bigPayload),
+ want: 32768,
+ is64Bit: true,
+ },
+ {
+ name: "Overflow Case - Data is 4GB + small amount",
+ maxPayload: 32768,
+ dataLen: int(bigPayload + 100),
+ want: 32768,
+ is64Bit: true,
+ },
+ }
+
+ is64Bit := bits.UintSize == 64
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.is64Bit && !is64Bit {
+ t.Skip("Skipping test requiring 64-bit int")
+ }
+ got := minPayloadSize(tt.maxPayload, tt.dataLen)
+ if got != tt.want {
+ t.Errorf("minPayloadSize(%d, %d) = %d; want %d", tt.maxPayload, tt.dataLen, got, tt.want)
+ }
+ })
+ }
+}
+
+// TestWriteExtendedNoInfiniteLoopOnLargeWrite is an end-to-end regression
+// test for the integer-overflow bug in WriteExtended. Before the fix, a
+// write whose len(data) was a multiple of 2^32 caused minPayloadSize to
+// return 0; WriteExtended then spun forever, reserving 0 bytes per
+// iteration and never advancing the data slice.
+//
+// We exercise the real WriteExtended path with a slice whose declared
+// length is exactly 2^32. Allocating 4 GiB is unnecessary: each iteration
+// only reads up to maxRemotePayload bytes from the head of the slice, and
+// the loop blocks in remoteWin.reserve() once the channel window is
+// exhausted — before the slice base advances past the underlying buffer.
+//
+// With the fix, the loop blocks in reserve(); we detect that via
+// waitWriterBlocked(), then close the window to let WriteExtended return.
+// With the bug, the loop never blocks and the test times out.
+//
+//go:nocheckptr
+func TestWriteExtendedNoInfiniteLoopOnLargeWrite(t *testing.T) {
+ if bits.UintSize < 64 {
+ t.Skip("test requires 64-bit int to construct a slice with len >= 2^32")
+ }
+
+ reader, writer, mux := channelPair(t)
+ defer reader.Close()
+ defer writer.Close()
+ defer mux.Close()
+
+ // Sized to hold the full pre-update remote window so that no iteration
+ // reads past the backing buffer before reserve() blocks.
+ backing := make([]byte, channelWindowSize)
+ var bigLen int64 = 1 << 32
+ bigSlice := unsafe.Slice(&backing[0], int(bigLen))
+
+ done := make(chan int, 1)
+ go func() {
+ n, _ := writer.Write(bigSlice)
+ done <- n
+ }()
+
+ blocked := make(chan struct{})
+ go func() {
+ writer.remoteWin.waitWriterBlocked()
+ close(blocked)
+ }()
+
+ select {
+ case <-blocked:
+ // Good — the loop made progress and is now blocked in reserve().
+ // Close the window to let WriteExtended return.
+ writer.remoteWin.close()
+ case <-time.After(2 * time.Second):
+ t.Fatal("WriteExtended did not block in reserve within 2s — minPayloadSize likely returned 0 (integer overflow regression)")
+ }
+
+ select {
+ case n := <-done:
+ if n == 0 {
+ t.Fatalf("WriteExtended returned n=0; expected progress")
+ }
+ case <-time.After(2 * time.Second):
+ t.Fatal("WriteExtended did not return after closing the window")
+ }
+}
--
2.45.4

7 changes: 6 additions & 1 deletion SPECS/cert-manager/cert-manager.spec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Summary: Automatically provision and manage TLS certificates in Kubernetes
Name: cert-manager
Version: 1.12.15
Release: 7%{?dist}
Release: 8%{?dist}
License: ASL 2.0
Vendor: Microsoft Corporation
Distribution: Azure Linux
Expand All @@ -24,6 +24,8 @@ Patch7: CVE-2025-11065.patch
Patch8: CVE-2025-47911.patch
Patch9: CVE-2025-58190.patch
Patch10: CVE-2026-35469.patch
Patch11: CVE-2026-39830.patch
Patch12: CVE-2026-39834.patch

BuildRequires: golang
Requires: %{name}-acmesolver
Expand Down Expand Up @@ -115,6 +117,9 @@ install -D -m0755 bin/webhook %{buildroot}%{_bindir}/
%{_bindir}/webhook

%changelog
* Wed May 27 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 1.12.15-8
- Patch for CVE-2026-39834, CVE-2026-39830

* Thu May 07 2026 Jyoti Kanase <v-jykanase@microsoft.com> - 1.12.15-7
- Patch for CVE-2026-35469

Expand Down
Loading