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/packer/CVE-2026-39830.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
From 6b6de254808d4738c085d78e7d429bac13937fb0 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/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

133 changes: 133 additions & 0 deletions SPECS/packer/CVE-2026-39832.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
From b105c320e704440ccf41a4cf577a1c98e92d7492 Mon Sep 17 00:00:00 2001
From: Nicola <nicola.murino@gmail.com>
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 <neal@golang.org>
Reviewed-by: Neal Patel <nealpatel@google.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: David Chase <drchase@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/golang/crypto/commit/a1ce0fee129597fdea8dfd58d71b6b607de6bdce.patch
---
internal/hcp/api/client_test.go | 60 +++++++++++++++++++
.../golang.org/x/crypto/ssh/agent/client.go | 7 +++
2 files changed, 67 insertions(+)

diff --git a/internal/hcp/api/client_test.go b/internal/hcp/api/client_test.go
index 16d321b..a4a27a0 100644
--- a/internal/hcp/api/client_test.go
+++ b/internal/hcp/api/client_test.go
@@ -81,3 +81,63 @@ func TestGetOldestProject(t *testing.T) {
})
}
}
+
+// 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 31bd7e8..d879c40 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

Loading
Loading