From 5a19fe80da260375353037dd44223a024dd66bd0 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Wed, 8 Jul 2026 21:17:42 +0500
Subject: [PATCH v2026-07-08 2/2] injection_points: test that pre-auth backends
 hold connection slots

A backend claims its PGPROC in InitProcessPhase2(), before authentication,
so max_connections must bound in-flight connections, not just authenticated
sessions.  Add a TAP test that fills every slot with connections frozen
before authentication, checks that one more is refused with "too many
clients", and that releasing one frees the slot again.

Such a backend cannot be driven or identified over SQL or pg_stat_activity,
so the test holds, observes and releases it through the init-pre-auth wait
point and the module's filesystem markers.
---
 src/test/modules/injection_points/meson.build |   1 +
 .../injection_points/t/002_preauth_slots.pl   | 147 ++++++++++++++++++
 2 files changed, 148 insertions(+)
 create mode 100644 src/test/modules/injection_points/t/002_preauth_slots.pl

diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index f6b53e1a33f..6074b373393 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -66,6 +66,7 @@ tests += {
     },
     'tests': [
       't/001_wait_without_sql.pl',
+      't/002_preauth_slots.pl',
     ],
   },
 }
diff --git a/src/test/modules/injection_points/t/002_preauth_slots.pl b/src/test/modules/injection_points/t/002_preauth_slots.pl
new file mode 100644
index 00000000000..5c5d46bef68
--- /dev/null
+++ b/src/test/modules/injection_points/t/002_preauth_slots.pl
@@ -0,0 +1,147 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Check that a connection which has not authenticated yet already counts
+# against max_connections.
+#
+# A backend claims its PGPROC entry in InitProcessPhase2(), and "sorry, too
+# many clients already" is reported at slot acquisition time -- both happen
+# before authentication.  The invariant under test is therefore that
+# max_connections bounds all in-flight connections, not just authenticated
+# sessions.  If that ever regressed (e.g. slots were accounted only after
+# authentication), unauthenticated clients could overrun the limit, and the
+# reserved_connections logic, which counts free PGPROCs including the pre-auth
+# ones, would hand out reserved slots it should not.
+#
+# Testing this needs a connection frozen in the narrow window after it has
+# taken a slot but before it authenticates, and such a backend cannot be
+# driven or even identified over SQL or pg_stat_activity.  The injection_points
+# module's "init-pre-auth" wait point and its filesystem markers are just the
+# tool used to hold a backend there and to observe/release it out of process.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Small, fully deterministic connection budget.  Client backends draw from the
+# regular PGPROC freelist sized by max_connections; autovacuum, background
+# workers and walsenders use separate pools, so this arithmetic is exact.
+my $max_conn = 4;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+	'postgresql.conf', qq[
+autovacuum = off
+max_connections = $max_conn
+superuser_reserved_connections = 0
+reserved_connections = 0
+]);
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $inj_dir = $node->data_dir . '/pg_injection_points/init-pre-auth';
+my $timeout_us = $PostgreSQL::Test::Utils::timeout_default * 1_000_000;
+
+# Return the set of pids currently parked at init-pre-auth, as a hash ref.
+sub parked_pids
+{
+	my %pids;
+	if (opendir(my $dh, $inj_dir))
+	{
+		%pids = map { $_ => 1 } grep { /^\d+$/ } readdir $dh;
+		closedir $dh;
+	}
+	return \%pids;
+}
+
+# Wait until a pid that is not in %$before shows up, and return it.
+sub wait_for_new_parked_pid
+{
+	my ($before) = @_;
+	my $waited = 0;
+	while (1)
+	{
+		my $now = parked_pids();
+		for my $pid (keys %$now)
+		{
+			return $pid unless $before->{$pid};
+		}
+		die "timed out waiting for a backend to park at init-pre-auth"
+		  if $waited > $timeout_us;
+		usleep(10_000);
+		$waited += 10_000;
+	}
+}
+
+# The controller session stays connected for the whole armed window; it arms
+# the wait point and never exits while it is attached, so it never parks.
+my $ctl = $node->background_psql('postgres');
+$ctl->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
+
+# Open victims that will hang before authentication, each consuming one slot.
+# We do not (indeed cannot) authenticate them; we only learn their pids from
+# the marker directory.  With the controller holding one slot, max_connections
+# - 1 victims exactly fill the remaining slots.
+my @victims;
+my @victim_pids;
+for (my $i = 0; $i < $max_conn - 1; $i++)
+{
+	my $before = parked_pids();
+	my $v = $node->background_psql('postgres', wait => 0);
+	push @victims, $v;
+	push @victim_pids, wait_for_new_parked_pid($before);
+}
+
+is(scalar(@victim_pids), $max_conn - 1,
+	'all pre-auth victims are parked and visible in the filesystem');
+
+# Every slot is now taken by pre-authentication backends plus the controller.
+# A fresh connection is refused before it ever reaches the wait point, which
+# proves the parked backends really do hold connection slots.
+$node->connect_fails(
+	'dbname=postgres',
+	'pre-auth backends occupy connection slots',
+	expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# Detach the point so that released backends, and the probe connection below,
+# do not park again.  Backends already parked keep waiting on their own marker
+# files, so they keep holding their slots until we remove those files.
+$ctl->query_safe("SELECT injection_points_detach('init-pre-auth')");
+
+# Let one pre-auth backend proceed (unlink its marker) and disconnect.  If the
+# slot it was holding while unauthenticated is now reusable, the connection
+# limit was accounting that pre-auth backend and nothing else.
+my $freed_pid = shift @victim_pids;
+my $freed_victim = shift @victims;
+unlink "$inj_dir/$freed_pid" or die "unlink $inj_dir/$freed_pid: $!";
+$freed_victim->wait_connect;
+$freed_victim->quit;
+
+is($node->safe_psql('postgres', 'SELECT 1'),
+	'1', 'freed pre-auth slot becomes available for a new connection');
+
+# Release and reap the remaining victims, then shut everything down.
+while (@victims)
+{
+	my $pid = shift @victim_pids;
+	my $v = shift @victims;
+	unlink "$inj_dir/$pid";
+	eval {
+		$v->wait_connect;
+		$v->quit;
+	};
+}
+$ctl->quit;
+$node->stop;
+
+done_testing();
-- 
2.50.1 (Apple Git-155)

