ANT-2026-CJQWKW82 · postgres/postgres

denial-of-service high

CVE-2026-6479 GHSA-hwfh-mh4f-m67f

Severity Claude high · Security research firm high · Maintainer high

Discovered by Claude Opus 4.6

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Calif.

ANT-2026-CJQWKW82: Pre-auth unbounded recursion in ProcessStartupPacket: alternating SSL/GSS negotiation requests cause infinite recursion when both are rejected. ssl_done/gss_done flags oscillate (true,false)->(false,true) endlessly. No check_stack_depth. Pre-authentication.

Unauthenticated attacker alternates SSL and GSS request packets. After ~40K-80K exchanges the backend crashes from stack overflow (SIGSEGV). On localhost takes seconds. AuthenticationTimeout (60s) is only time bound.

Target

Project: postgres/postgres
Commit: 5241616289cc64e0
Location: src/backend/tcop/backend_startup.c:637
Discovery: static analysis — not yet dynamically reproduced

Technical Details

When SSL is rejected, line 637 recurses with ProcessStartupPacket(port, true, SSLok == 'S') → (true,false); when GSS is rejected, line 693 recurses with ProcessStartupPacket(port, GSSok == 'G', true) → (false,true). The guards at lines 574 (!ssl_done) and 639 (!gss_done) therefore never both stay true, so alternating requests re-enter forever. No check_stack_depth(), counter, or iteration cap exists in the path, so the recursion terminates only via stack exhaustion (SIGSEGV).

Reproduction

This finding was identified by static analysis and has not yet been dynamically reproduced. The Technical Details section above describes the code path; a trigger input is not included.

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-CJQWKW82.


Reference: ANT-2026-CJQWKW82
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Calif.

Verdict
true positive
Severity
high
UPSTREAM FIX

The change that resolved this finding.

diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 5abf276c89848..a810e41a9040e 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -496,6 +496,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 	ProtocolVersion proto;
 	MemoryContext oldcontext;
 
+retry:
 	pq_startmsgread();
 
 	/*
@@ -616,6 +617,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 #endif
 
 		pfree(buf);
+		buf = NULL;
 
 		/*
 		 * At this point we should have no data already buffered.  If we do,
@@ -634,7 +636,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 		 * another SSL negotiation request, and a GSS request should only
 		 * follow if SSL was rejected (client may negotiate in either order)
 		 */
-		return ProcessStartupPacket(port, true, SSLok == 'S');
+		ssl_done = true;
+		if (SSLok == 'S')
+		{
+			/*
+			 * We are done with SSL and negotiated correctly, so consider the
+			 * same for GSS.
+			 */
+			gss_done = true;
+		}
+		goto retry;
 	}
 	else if (proto == NEGOTIATE_GSS_CODE && !gss_done)
 	{
@@ -672,6 +683,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 #endif
 
 		pfree(buf);
+		buf = NULL;
 
 		/*
 		 * At this point we should have no data already buffered.  If we do,
@@ -690,7 +702,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 		 * another GSS negotiation request, and an SSL request should only
 		 * follow if GSS was rejected (client may negotiate in either order)
 		 */
-		return ProcessStartupPacket(port, GSSok == 'G', true);
+		gss_done = true;
+		if (GSSok == 'G')
+		{
+			/*
+			 * We are done with GSS and negotiated correctly, so consider the
+			 * same for SSL.
+			 */
+			ssl_done = true;
+		}
+		goto retry;
 	}
 
 	/* Could add additional special packet types here */
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
index d2709867da71c..fa30883b601bd 100644
--- a/src/test/postmaster/meson.build
+++ b/src/test/postmaster/meson.build
@@ -9,6 +9,7 @@ tests += {
       't/001_basic.pl',
       't/002_connection_limits.pl',
       't/003_start_stop.pl',
+      't/004_negotiate.pl',
     ],
   },
 }
diff --git a/src/test/postmaster/t/004_negotiate.pl b/src/test/postmaster/t/004_negotiate.pl
new file mode 100644
index 0000000000000..949aa2ba19ac0
--- /dev/null
+++ b/src/test/postmaster/t/004_negotiate.pl
@@ -0,0 +1,83 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test the negotiation of combined SSL and GSS requests.  This test
+# relies on both SSL and GSS requests to be rejected first, followed
+# by more requests.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "log_min_messages = debug2");
+$node->append_conf('postgresql.conf',
+	"log_connections = 'receipt,authentication,authorization'");
+$node->append_conf('postgresql.conf', 'trace_connection_negotiation=on');
+$node->start;
+
+if (!$node->raw_connect_works())
+{
+	plan skip_all => "this test requires working raw_connect()";
+}
+
+my $sock = $node->raw_connect();
+
+# SSLRequest: packet length followed by NEGOTIATE_SSL_CODE.
+my $ssl_request = pack("Nnn", 8, 1234, 5679);
+
+# GSSENCRequest: packet length followed by NEGOTIATE_GSS_CODE.
+my $gss_request = pack("Nnn", 8, 1234, 5680);
+
+# Send SSLRequest, reject or bypass.
+$sock->send($ssl_request);
+my $reply = "";
+$sock->recv($reply, 1);
+if ($reply ne 'N')
+{
+	$sock->close();
+	plan skip_all =>
+	  "server accepted SSL; test requires SSL to be rejected";
+}
+
+# Send GSSENCRequest, reject or bypass test.
+$sock->send($gss_request);
+$reply = "";
+$sock->recv($reply, 1);
+if ($reply ne 'N')
+{
+	$sock->close();
+	plan skip_all =>
+	  "server accepted GSS; test requires GSS to be rejected";
+}
+
+my $log_offset = -s $node->logfile;
+
+# Send a second SSLRequest, now that we know that both SSL and GSS have
+# been rejected for this connection.  We are done with both requests, so
+# extra requests will be rejected and fail with an invalid protocol
+# version, and the connection should be closed by the server.
+$sock->send($ssl_request);
+
+# Try to read a response, there should be nothing, and certainly not an
+# extra 'N' message indicating a rejection.
+$reply = "";
+my $bytes = $sock->recv($reply, 1024);
+isnt($reply, 'N',
+	"server does not re-enter SSL negotiation after SSL+GSS were both tried");
+
+$sock->close();
+$node->wait_for_log(qr/FATAL: .* unsupported frontend protocol 1234.5679/,
+					$log_offset);
+
+# Check extra connection with a simple query.
+my $result = $node->safe_psql('postgres', 'select 1;');
+is($result, '1', 'server able to accept connection');
+ok($node->is_alive(), "server still running after negotiation attempt");
+
+$node->stop;
+
+done_testing();

https://github.com/postgres/postgres/commit/b63f25bddfebc67b1e78f86341a6aecb0e9fe576

TIMELINE

Dates from discovery through public reveal.

  1. 2026-04-01 Reported to tracker
  2. 2026-05-08 Sent to maintainer
  3. 2026-05-09 Maintainer acknowledged
  4. 2026-05-14 Patch released
  5. 2026-07-21 Publicly revealed
PROVENANCE

SHA-3-512 hash:

6ccb97193bc23f5185d8846690778b6b553b97c7a27eff1ae8a5e80c0f404193c5d139372722332faa52572e486f6d0512238b53e885233b5bf7d1c8d3589f5a

Committed 2026-05-08 00:09 PT

Revealed 2026-07-21 11:01 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-CJQWKW82",
  "bug_class": "Denial-of-service",
  "claude_severity": "high",
  "commit_sha": "5241616289cc64e0",
  "created_at": "2026-04-02T05:09:32+00:00",
  "description": "Unauthenticated attacker alternates SSL and GSS request packets. After ~40K-80K exchanges the backend crashes from stack overflow (SIGSEGV). On localhost takes seconds. AuthenticationTimeout (60s) is only time bound.",
  "discovered_at": null,
  "location": "src/backend/tcop/backend_startup.c:637",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "postgres/postgres",
  "reproduction": null,
  "technical_details": "When SSL is rejected, line 637 recurses with ProcessStartupPacket(port, true, SSLok == 'S') → (true,false); when GSS is rejected, line 693 recurses with ProcessStartupPacket(port, GSSok == 'G', true) → (false,true). The guards at lines 574 (!ssl_done) and 639 (!gss_done) therefore never both stay true, so alternating requests re-enter forever. No check_stack_depth(), counter, or iteration cap exists in the path, so the recursion terminates only via stack exhaustion (SIGSEGV).",
  "title": "Pre-auth unbounded recursion in ProcessStartupPacket: alternating SSL/GSS negotiation requests cause infinite recursion when both are rejected. ssl_done/gss_done flags oscillate (true,false)->(false,true) endlessly. No check_stack_depth. Pre-authentication.",
  "vendor_severity": "high"
}