diff --git a/SPECS/docker-compose/CVE-2026-39830.patch b/SPECS/docker-compose/CVE-2026-39830.patch new file mode 100644 index 00000000000..3b12d4b20a1 --- /dev/null +++ b/SPECS/docker-compose/CVE-2026-39830.patch @@ -0,0 +1,104 @@ +From c513a2c5e4f66a7d7ea13db6f814b4657d19bdb8 Mon Sep 17 00:00:00 2001 +From: Nicola Murino +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 +Reviewed-by: Neal Patel +LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com +Signed-off-by: Azure Linux Security Servicing Account +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/vendor/golang.org/x/crypto/ssh/mux.go b/vendor/golang.org/x/crypto/ssh/mux.go +index d2d24c6..3bc4afb 100644 +--- a/vendor/golang.org/x/crypto/ssh/mux.go ++++ b/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 + diff --git a/SPECS/docker-compose/CVE-2026-39832.patch b/SPECS/docker-compose/CVE-2026-39832.patch new file mode 100644 index 00000000000..b4542c9cc67 --- /dev/null +++ b/SPECS/docker-compose/CVE-2026-39832.patch @@ -0,0 +1,133 @@ +From 30109bdbc2651fed2e2366c2eefabdf68f41ea2d Mon Sep 17 00:00:00 2001 +From: Nicola +Date: Tue, 27 Jan 2026 12:15:18 +0100 +Subject: [PATCH] ssh/agent: preserve constraint extensions when adding keys + +The client Add method only serialized the lifetime and confirm +constraints and silently dropped AddedKey.ConstraintExtensions before +sending the SSH_AGENTC_ADD_IDENTITY request. As a result the remote +agent always received the key with no extension constraints, regardless +of what the caller requested. + +Applications that add a key believing custom constraint extensions +(such as restrict-destination-v00@openssh.com) would be enforced +instead loaded a completely unrestricted key into the agent. For +example, an administrator forwarding their agent into an untrusted jump +host and trying to limit the forwarded key with restrict-destination +never had that restriction reach the agent: any user or compromised +process on that host could make the agent sign arbitrary challenges. + +Serialize each entry in key.ConstraintExtensions as an +agentConstrainExtension constraint so the constraints reach the agent, +and add a round-trip regression test that verifies the extensions +survive client serialization and server parsing. + +This issue was found during a security audit by NCC Group Cryptography +Services, sponsored by Teleport. + +Updates CVE-2026-39832 +Updates golang/go#79435 + +Change-Id: I14c5583b106cbf0d282d2ba01e000e0f586f08c7 +Reviewed-on: https://go-review.googlesource.com/c/crypto/+/778640 +Reviewed-by: Neal Patel +Reviewed-by: Neal Patel +Reviewed-by: Keith Randall +Reviewed-by: David Chase +LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/golang/crypto/commit/a1ce0fee129597fdea8dfd58d71b6b607de6bdce.patch +--- + internal/desktop/client_test.go | 60 +++++++++++++++++++ + .../golang.org/x/crypto/ssh/agent/client.go | 7 +++ + 2 files changed, 67 insertions(+) + +diff --git a/internal/desktop/client_test.go b/internal/desktop/client_test.go +index 0355dd5..4e96fcc 100644 +--- a/internal/desktop/client_test.go ++++ b/internal/desktop/client_test.go +@@ -50,3 +50,63 @@ func TestClientPing(t *testing.T) { + serverTime := time.Unix(0, ret.ServerTime) + require.True(t, now.Before(serverTime)) + } ++ ++// capturingAgent records the AddedKey observed on the server side of the ++// agent protocol. It forwards to a keyring with constraint fields stripped, ++// so the keyring's own rejection of unsupported constraints does not ++// interfere with what this test is measuring: the client-side wire ++// serialization of ConstraintExtensions. ++type capturingAgent struct { ++ Agent ++ lastAdd AddedKey ++} ++ ++func newCapturingAgent() *capturingAgent { ++ return &capturingAgent{Agent: NewKeyring()} ++} ++ ++func (a *capturingAgent) Add(key AddedKey) error { ++ a.lastAdd = key ++ stripped := key ++ stripped.ConstraintExtensions = nil ++ stripped.ConfirmBeforeUse = false ++ return a.Agent.Add(stripped) ++} ++ ++// TestAddConstraintExtensionsWireFormat verifies that client.Add serializes ++// ConstraintExtensions into the SSH_AGENTC_ADD_IDENTITY payload and the ++// server deserializes them back into the AddedKey delivered to the backend. ++// Regressions in the client marshal loop (missing, swapped fields, wrong ++// framing) would be invisible to a keyring-based rejection test, which ++// signals only "extensions were present", not "the right ones arrived". ++func TestAddConstraintExtensionsWireFormat(t *testing.T) { ++ capturing := newCapturingAgent() ++ client, cleanup := startAgent(t, capturing) ++ defer cleanup() ++ ++ constraints := []ConstraintExtension{ ++ {ExtensionName: "ext-one@example.com", ExtensionDetails: []byte("details-one")}, ++ {ExtensionName: "ext-two@example.com", ExtensionDetails: []byte("\x00\x01\x02\xff")}, ++ } ++ ++ if err := client.Add(AddedKey{ ++ PrivateKey: testPrivateKeys["rsa"], ++ Comment: "wire-format-test", ++ ConstraintExtensions: constraints, ++ }); err != nil { ++ t.Fatalf("client.Add: %v", err) ++ } ++ ++ got := capturing.lastAdd.ConstraintExtensions ++ if len(got) != len(constraints) { ++ t.Fatalf("server received %d extensions, want %d", len(got), len(constraints)) ++ } ++ for i, want := range constraints { ++ if got[i].ExtensionName != want.ExtensionName { ++ t.Errorf("extension[%d] name: got %q, want %q", i, got[i].ExtensionName, want.ExtensionName) ++ } ++ if !bytes.Equal(got[i].ExtensionDetails, want.ExtensionDetails) { ++ t.Errorf("extension[%d] details: got %x, want %x", i, got[i].ExtensionDetails, want.ExtensionDetails) ++ } ++ } ++} +diff --git a/vendor/golang.org/x/crypto/ssh/agent/client.go b/vendor/golang.org/x/crypto/ssh/agent/client.go +index 6dc73e0..d9e7f73 100644 +--- a/vendor/golang.org/x/crypto/ssh/agent/client.go ++++ b/vendor/golang.org/x/crypto/ssh/agent/client.go +@@ -663,6 +663,13 @@ func (c *client) Add(key AddedKey) error { + constraints = append(constraints, agentConstrainConfirm) + } + ++ for _, ext := range key.ConstraintExtensions { ++ constraints = append(constraints, ssh.Marshal(constrainExtensionAgentMsg{ ++ ExtensionName: ext.ExtensionName, ++ ExtensionDetails: ext.ExtensionDetails, ++ })...) ++ } ++ + cert := key.Certificate + if cert == nil { + return c.insertKey(key.PrivateKey, key.Comment, constraints) +-- +2.45.4 + diff --git a/SPECS/docker-compose/CVE-2026-39834.patch b/SPECS/docker-compose/CVE-2026-39834.patch new file mode 100644 index 00000000000..f15660e14af --- /dev/null +++ b/SPECS/docker-compose/CVE-2026-39834.patch @@ -0,0 +1,222 @@ +From fda053973b076d1efd1e865cb3a452dbc36e229d Mon Sep 17 00:00:00 2001 +From: Nicola Murino +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 +Reviewed-by: Roland Shoemaker +LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/TheDegenerateDev5150/crypto/commit/e052873987615dc96fe67607a9a6adb76311344f.patch +--- + ssh/channel_test.go | 140 ++++++++++++++++++++++ + vendor/golang.org/x/crypto/ssh/channel.go | 16 ++- + 2 files changed, 151 insertions(+), 5 deletions(-) + create mode 100644 ssh/channel_test.go + +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") ++ } ++} +diff --git a/vendor/golang.org/x/crypto/ssh/channel.go b/vendor/golang.org/x/crypto/ssh/channel.go +index cc0bb7a..3967b65 100644 +--- a/vendor/golang.org/x/crypto/ssh/channel.go ++++ b/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 + } +-- +2.45.4 + diff --git a/SPECS/docker-compose/docker-compose.spec b/SPECS/docker-compose/docker-compose.spec index e67d5418582..792cea19af9 100644 --- a/SPECS/docker-compose/docker-compose.spec +++ b/SPECS/docker-compose/docker-compose.spec @@ -1,7 +1,7 @@ Summary: Define and run multi-container applications with Docker Name: docker-compose Version: 2.27.0 -Release: 10%{?dist} +Release: 11%{?dist} License: ASL 2.0 Vendor: Microsoft Corporation Distribution: Azure Linux @@ -23,6 +23,9 @@ Patch7: CVE-2025-47911.patch Patch8: CVE-2025-58190.patch Patch9: CVE-2026-39882.patch Patch10: CVE-2026-35469.patch +Patch11: CVE-2026-39830.patch +Patch12: CVE-2026-39832.patch +Patch13: CVE-2026-39834.patch BuildRequires: golang Requires: docker-cli @@ -56,6 +59,9 @@ install -D -m0755 bin/build/docker-compose %{buildroot}/%{_libexecdir}/docker/cl %{_libexecdir}/docker/cli-plugins/docker-compose %changelog +* Wed May 27 2026 Azure Linux Security Servicing Account - 2.27.0-11 +- Patch for CVE-2026-39834, CVE-2026-39832, CVE-2026-39830 + * Wed May 06 2026 Azure Linux Security Servicing Account - 2.27.0-10 - Patch for CVE-2026-35469