CommitFest 6207 — Windows Socket Handle Inheritance Fix (review)

code-review · generated from canonical Markdown

Type
code-review
Source
postgresql-master
Slug
commitfest-6207-review
Tags
code-reviewcommitfestsecuritysocketwindows
Updated
2026-10-02T18:09:55.536309+00:00
Verdict / Summarywaiting-on-author
Front matter
schema: review-report-v2
target: CommitFest 6207 — Windows socket handle inheritance fix
head: 6f3bdadaadc4692050ca20dff53d3890088b9272
base: 6f3bdadaadc4692050ca20dff53d3890088b9272
verdict: waiting-on-author
scope_identity: diff-sha:fa23de5c6dfe69fba8e39f62b0875bb20a632fe808e88631a04c515e0913fb1d

CommitFest 6207 — Windows Socket Handle Inheritance Fix

Verdict: waiting-on-author Blocker counts: verified=0, unverified=0 Findings: 2 MAJOR (tests), 2 MINOR (security/maintainability), 1 MINOR (tests), 3 NIT Thread: [pgsql-hackers 626963](https://www.postgresql.org/message-id/flat/1387e1ea-ee05-4f5b-a1c4-6c91fddb6d07%40gmail.com) — reconciled, no unresolved objections Coverage: not-applicable (WIN32-guarded code, non-instrumentable on Linux build host)

Patch summary

The patch prevents Windows socket handle inheritance to child processes:

  1. pgwin32_socket() now passes WSA_FLAG_NO_HANDLE_INHERIT to WSASocket() — newly created sockets (including the listening socket) are non-inheritable.
  2. BackendInitialize() calls SetHandleInformation(... HANDLE_FLAG_INHERIT, 0) on the client socket under #ifdef WIN32 — clears inheritance on the backend's client socket (which read_inheritable_socket recreates as inheritable via WSASocket(FROM_PROTOCOL_INFO,...,0) with dwFlags=0).
  3. New TAP test src/bin/pg_ctl/t/005_socket_handle_inheritance.pl (Windows-only).

Core fix correctness: The two-pronged approach (creation-time flag + post-inheritance clear) is correct for the stated zombie-listening-port bug. The approach is endorsed on-list by Thomas Munro (thread 626963, 2025-11-07). No catalog/ABI change; no catversion bump needed.

Findings

IDSeverityClassLocationVerificationRepair Validation
PAT-1MAJORtestssrc/bin/pg_ctl/t/005_socket_handle_inheritance.pl:38-42verifiednot-run
PAT-2MAJORtestssrc/bin/pg_ctl/t/005_socket_handle_inheritance.pl:38-42verifiednot-run
PAT-3MINORtestssrc/bin/pg_ctl/t/005_socket_handle_inheritance.pl:125-133verifiednot-run
PAT-4NITtestssrc/bin/pg_ctl/t/005_socket_handle_inheritance.pl:134verifiednot-run
PAT-5MINORmaintainabilitysrc/bin/pg_ctl/meson.build:30verifiednot-run
PAT-6MINORsecuritysrc/backend/tcop/backend_startup.c:186-189verifiednot-run
PAT-7MINORsecuritysrc/backend/port/win32/socket.c:341-359verifiednot-run
PAT-8NITcorrectnesssrc/backend/tcop/backend_startup.c:182-185verifiednot-run
PAT-9NITdocssrc/backend/port/win32/socket.c:300-301verifiednot-run
PAT-1

— MAJOR — tests — src/bin/pg_ctl/t/005_socket_handle_inheritance.pl:38-42

Mechanism: Test uses \copy (psql client-side meta-command) to spawn a program; the spawned process is a child of psql, not a backend. The patched server code (pgwin32_socket, BackendInitialize SetHandleInformation) is never exercised.

Failure mode: The test passes or fails independently of the fix. Whatever result it produces is unrelated to the patched code paths.

Verification status: verified Verification method: source-analysis Verification evidence: src/bin/psql/copy.c:263-265,289-293 (\copy processed client-side via popen in psql); src/test/perl/PostgreSQL/Test/Cluster.pm:2077-2102 (safe_psql invokes psql which processes meta-commands client-side). Contrast with server-side COPY TO PROGRAM in src/backend/commands/copyto.c:1166 (OpenPipeStream).

Repair instruction:

  • Files: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl, src/test/recovery/t/058_socket_handle_inheritance.pl, src/test/recovery/meson.build, src/bin/pg_ctl/meson.build
  • Locations: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl (delete), src/test/recovery/t/058_socket_handle_inheritance.pl (new, uses server-side COPY TO PROGRAM), src/test/recovery/meson.build (add t/058 entry), src/bin/pg_ctl/meson.build (remove staged t/005 entry)
  • Exact edits: Delete the old test. Create a new test under src/test/recovery/t/058_socket_handle_inheritance.pl that uses server-side COPY (SELECT 1) TO PROGRAM so the child is spawned by the backend, exercising the patched BackendInitialize SetHandleInformation and pgwin32_socket code paths.
  • Intended postcondition: The child process is spawned by a backend via server-side COPY TO PROGRAM, exercising the patched server code.

Code example: status=provided, relationship=illustrative

perl
$node->safe_psql('postgres', qq{COPY (SELECT 1) TO PROGRAM '$copy_cmd';});

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

diff
diff --git a/src/bin/pg_ctl/meson.build b/src/bin/pg_ctl/meson.build
index 69fa7a28427..e7468116b7 100644
--- a/src/bin/pg_ctl/meson.build
+++ b/src/bin/pg_ctl/meson.build
@@ -27,7 +27,6 @@ tests += {
       't/002_status.pl',
       't/003_promote.pl',
       't/004_logrotate.pl',
-      't/005_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl b/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
deleted file mode 100644
--- a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (c) 2025, PostgreSQL Global Development Group
-
-# Test that socket handles are not inherited by child processes on Windows.
-#
-# Without the fix, child processes spawned via COPY TO PROGRAM inherit socket
-# handles from the backend. Windows reference counting prevents these sockets
-# from being freed when the postmaster exits, leaving the port bound to a dead
-# process (a "zombie" binding). This test verifies that after killing the
-# postmaster while a child process is still running, the listening port is
-# immediately freed rather than remaining in a zombie state.
-
-use strict;
-use warnings;
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-use Time::HiRes qw(sleep);
-
-# This test is Windows-specific
-if ($^O ne 'MSWin32')
-{
-	plan skip_all => 'test is specific to Windows socket handle inheritance';
-}
-
-my $node = PostgreSQL::Test::Cluster->new('main');
-$node->init;
-$node->start;
-
-# Get the port number for verification
-my $port = $node->port;
-
-# Spawn a long-running child process via COPY TO PROGRAM that will outlive
-# the postmaster. Without the fix, this child inherits socket handles.
-my $marker_file = $node->data_dir . '/ps_marker.txt';
-unlink $marker_file if -e $marker_file;
-
-$node->safe_psql(
-	'postgres',
-	qq{\\copy (select 1) to program 'powershell -Command "echo marker > $marker_file; Start-Sleep 120"'}
-);
-
-# Wait for PowerShell to spawn
-my $ps_spawned = 0;
-for (my $i = 0; $i < 100; $i++)
-{
-	if (-e $marker_file)
-	{
-		$ps_spawned = 1;
-		last;
-	}
-	sleep 0.1;
-}
-
-ok($ps_spawned, 'child process spawned successfully');
-
-# Stop the postmaster (simulates a crash), leaving the child process running.
-$node->stop('immediate');
-sleep 0.5;
-
-# Verify that the listening port is freed immediately. With the bug, the port
-# remains bound to the dead postmaster PID because the child process inherited
-# the socket handles. With the fix, the port is freed because socket handles
-# were not inherited.
-my $netstat_output = `netstat -ano | findstr ":$port.*LISTENING"`;
-
-if ($netstat_output)
-{
-	fail('listening port remains bound after postmaster exit (zombie port)');
-	diag("Port is still bound - socket handles were inherited by child process");
-	diag("netstat output:\n$netstat_output");
-
-	if ($netstat_output =~ /LISTENING\s+(\d+)/)
-	{
-		my $bound_pid = $1;
-		my $process_name = get_process_name($bound_pid);
-
-		if ($process_name eq 'unknown' || $process_name eq '')
-		{
-			diag("Port bound to dead process (PID $bound_pid) - zombie binding detected");
-		}
-		else
-		{
-			diag("Port bound to: $process_name (PID $bound_pid)");
-		}
-	}
-}
-else
-{
-	pass('listening port freed immediately after postmaster exit');
-}
-
-# Additional verification: Confirm the port is actually available for binding.
-# This tests the real-world scenario that matters to users.
-my $can_bind = test_port_available($port);
-ok($can_bind, "port $port is available for new connections");
-
-# Cleanup
-cleanup_powershell_processes();
-unlink $marker_file if -e $marker_file;
-
-done_testing();
-
-# Test if port can actually be bound
-sub test_port_available
-{
-	my ($port) = @_;
-
-	use Socket;
-
-	socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or return 0;
-	setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
-
-	my $addr = sockaddr_in($port, INADDR_ANY);
-	my $result = bind($sock, $addr);
-	close($sock);
-	
-	return $result ? 1 : 0;
-}
-
-# Get process name by PID
-sub get_process_name
-{
-	my ($pid) = @_;
-	my $name = `powershell -Command "(Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName" 2>nul`;
-	chomp $name;
-	return $name || 'unknown';
-}
-
-# Clean up test child processes
-sub cleanup_powershell_processes
-{
-	system('powershell -Command "Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {$_.Id -ne $PID} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul');
-}
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 123456789ab..23456789abc 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -68,6 +68,7 @@
       't/056_standby_snapshot_export.pl',
       't/057_snapshot_commit_race.pl',
+      't/058_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/058_socket_handle_inheritance.pl b/src/test/recovery/t/058_socket_handle_inheritance.pl
new file mode 100644
--- /dev/null
+++ b/src/test/recovery/t/058_socket_handle_inheritance.pl
@@ -0,0 +1,80 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test that socket handles are not inherited by child processes on Windows.
+#
+# Without the fix, a detached grandchild spawned via COPY TO PROGRAM
+# inherits the listening socket; the kernel's reference counting keeps the
+# port bound after the postmaster dies, and a restart on the same port fails.
+# With the fix, the listening socket is not inherited and the restart succeeds.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+plan skip_all => 'this test exercises Windows socket handle inheritance'
+  if $^O ne 'MSWin32';
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $port = $node->port;
+my $data_dir = $node->data_dir;
+my $marker_file = "$data_dir/copy_marker.txt";
+my $pidfile     = "$data_dir/copy_grandchild_pid.txt";
+unlink $marker_file, $pidfile;
+
+my $copy_cmd =
+    q{powershell -NoProfile -Command "}
+  . qq{Set-Content -Path '$marker_file' -Value 'started' -Encoding ASCII; }
+  . qq{\$p = Start-Process -FilePath powershell -ArgumentList '-NoProfile','-Command','Start-Sleep 300' -WindowStyle Hidden -PassThru; }
+  . qq{\$p.Id | Out-File -FilePath '$pidfile' -Encoding ASCII; }
+  . q{exit 0"}
+  . q{};
+
+$node->safe_psql(
+	'postgres',
+	qq{COPY (SELECT 1) TO PROGRAM '$copy_cmd';});
+
+my $grandchild_pid;
+ok( PostgreSQL::Test::Utils::poll_execute(
+		sub { -e $marker_file && -e $pidfile; }
+	),
+	'detached grandchild spawned by backend COPY TO PROGRAM'
+);
+
+{
+	open my $fh, '<', $pidfile or die "could not open $pidfile: $!";
+	$grandchild_pid = <$fh>;
+	close $fh;
+	chomp $grandchild_pid if defined $grandchild_pid;
+}
+diag("grandchild pid: $grandchild_pid") if defined $grandchild_pid;
+
+$node->kill9;
+sleep 0.5;
+
+my $restarted = $node->start(fail_ok => 1);
+ok($restarted, "postmaster restarted on port $port after crash with detached grandchild running");
+
+if (!$restarted)
+{
+	diag("restart failed - listening port was not freed; ");
+	diag("this indicates the detached grandchild inherited the listening socket");
+}
+
+$node->stop('immediate');
+if (defined $grandchild_pid && $grandchild_pid =~ /^\d+$/)
+{
+	system(
+		qq{powershell -NoProfile -Command "Stop-Process -Id $grandchild_pid -Force -ErrorAction SilentlyContinue" 2>$null}
+	);
+}
+unlink $marker_file, $pidfile;
+
+done_testing();
+

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-test-relocation.diff, git apply patch-test-relocation.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. Apply-check and diff-check confirm structural validity. Targeted-test cannot run: the test is Windows-only (plan skip_all on non-MSWin32) and the build host is Linux.
  • Reason: Windows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
  • Tested patch SHA-256: 106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

Test example: status=provided

  • Vehicle: tap
  • Files: src/test/recovery/t/058_socket_handle_inheritance.pl
  • Oracle: With the fix: detached grandchild does not inherit listening socket, port freed after kill9, restart succeeds. Without the fix: grandchild inherits listening socket, port stays bound, restart fails.
  • Baseline result: On Windows without fix: restart fails (port stuck). On Linux: test skips (plan skip_all).
  • Patched result: On Windows with fix: restart succeeds (port freed). On Linux: test skips.

---

PAT-2

— MAJOR — tests — src/bin/pg_ctl/t/005_socket_handle_inheritance.pl:38-42

Mechanism: Even with server-side COPY TO PROGRAM, pclose() in ClosePipeStream blocks until the child exits. The Start-Sleep 120 child keeps safe_psql blocked for 120 seconds, causing timeout or hang.

Failure mode: The test hangs for ~120s per run or times out, producing a spurious failure regardless of the fix.

Verification status: verified Verification method: source-analysis Verification evidence: src/backend/commands/copyto.c:721-741 (ClosePipeToProgram calls ClosePipeStream/pclose which blocks until child exits); src/test/perl/PostgreSQL/Test/Cluster.pm:2085-2091 (safe_psql blocks until psql returns).

Repair instruction:

  • Files: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl, src/test/recovery/t/058_socket_handle_inheritance.pl, src/test/recovery/meson.build, src/bin/pg_ctl/meson.build
  • Locations: COPY TO PROGRAM command in the new test
  • Exact edits: Use a detached PowerShell Start-Process grandchild that sleeps in the background while the outer powershell exits 0 immediately, so pclose() returns promptly and safe_psql does not block.
  • Intended postcondition: The COPY TO PROGRAM program exits promptly (outer powershell exits 0); a detached grandchild sleeps 300s in the background holding any inherited handles. safe_psql returns immediately.

Code example: status=provided, relationship=illustrative

perl
my $copy_cmd =
    q{powershell -NoProfile -Command "}
  . qq{Set-Content -Path '$marker_file' -Value 'started'; }
  . qq{\$p = Start-Process -FilePath powershell -ArgumentList '-Command','Start-Sleep 300' -PassThru; }
  . qq{\$p.Id | Out-File -FilePath '$pidfile'; }
  . q{exit 0"};

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

diff
diff --git a/src/bin/pg_ctl/meson.build b/src/bin/pg_ctl/meson.build
index 69fa7a28427..e7468116b7 100644
--- a/src/bin/pg_ctl/meson.build
+++ b/src/bin/pg_ctl/meson.build
@@ -27,7 +27,6 @@ tests += {
       't/002_status.pl',
       't/003_promote.pl',
       't/004_logrotate.pl',
-      't/005_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl b/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
deleted file mode 100644
--- a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (c) 2025, PostgreSQL Global Development Group
-
-# Test that socket handles are not inherited by child processes on Windows.
-#
-# Without the fix, child processes spawned via COPY TO PROGRAM inherit socket
-# handles from the backend. Windows reference counting prevents these sockets
-# from being freed when the postmaster exits, leaving the port bound to a dead
-# process (a "zombie" binding). This test verifies that after killing the
-# postmaster while a child process is still running, the listening port is
-# immediately freed rather than remaining in a zombie state.
-
-use strict;
-use warnings;
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-use Time::HiRes qw(sleep);
-
-# This test is Windows-specific
-if ($^O ne 'MSWin32')
-{
-	plan skip_all => 'test is specific to Windows socket handle inheritance';
-}
-
-my $node = PostgreSQL::Test::Cluster->new('main');
-$node->init;
-$node->start;
-
-# Get the port number for verification
-my $port = $node->port;
-
-# Spawn a long-running child process via COPY TO PROGRAM that will outlive
-# the postmaster. Without the fix, this child inherits socket handles.
-my $marker_file = $node->data_dir . '/ps_marker.txt';
-unlink $marker_file if -e $marker_file;
-
-$node->safe_psql(
-	'postgres',
-	qq{\\copy (select 1) to program 'powershell -Command "echo marker > $marker_file; Start-Sleep 120"'}
-);
-
-# Wait for PowerShell to spawn
-my $ps_spawned = 0;
-for (my $i = 0; $i < 100; $i++)
-{
-	if (-e $marker_file)
-	{
-		$ps_spawned = 1;
-		last;
-	}
-	sleep 0.1;
-}
-
-ok($ps_spawned, 'child process spawned successfully');
-
-# Stop the postmaster (simulates a crash), leaving the child process running.
-$node->stop('immediate');
-sleep 0.5;
-
-# Verify that the listening port is freed immediately. With the bug, the port
-# remains bound to the dead postmaster PID because the child process inherited
-# the socket handles. With the fix, the port is freed because socket handles
-# were not inherited.
-my $netstat_output = `netstat -ano | findstr ":$port.*LISTENING"`;
-
-if ($netstat_output)
-{
-	fail('listening port remains bound after postmaster exit (zombie port)');
-	diag("Port is still bound - socket handles were inherited by child process");
-	diag("netstat output:\n$netstat_output");
-
-	if ($netstat_output =~ /LISTENING\s+(\d+)/)
-	{
-		my $bound_pid = $1;
-		my $process_name = get_process_name($bound_pid);
-
-		if ($process_name eq 'unknown' || $process_name eq '')
-		{
-			diag("Port bound to dead process (PID $bound_pid) - zombie binding detected");
-		}
-		else
-		{
-			diag("Port bound to: $process_name (PID $bound_pid)");
-		}
-	}
-}
-else
-{
-	pass('listening port freed immediately after postmaster exit');
-}
-
-# Additional verification: Confirm the port is actually available for binding.
-# This tests the real-world scenario that matters to users.
-my $can_bind = test_port_available($port);
-ok($can_bind, "port $port is available for new connections");
-
-# Cleanup
-cleanup_powershell_processes();
-unlink $marker_file if -e $marker_file;
-
-done_testing();
-
-# Test if port can actually be bound
-sub test_port_available
-{
-	my ($port) = @_;
-
-	use Socket;
-
-	socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or return 0;
-	setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
-
-	my $addr = sockaddr_in($port, INADDR_ANY);
-	my $result = bind($sock, $addr);
-	close($sock);
-	
-	return $result ? 1 : 0;
-}
-
-# Get process name by PID
-sub get_process_name
-{
-	my ($pid) = @_;
-	my $name = `powershell -Command "(Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName" 2>nul`;
-	chomp $name;
-	return $name || 'unknown';
-}
-
-# Clean up test child processes
-sub cleanup_powershell_processes
-{
-	system('powershell -Command "Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {$_.Id -ne $PID} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul');
-}
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 123456789ab..23456789abc 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -68,6 +68,7 @@
       't/056_standby_snapshot_export.pl',
       't/057_snapshot_commit_race.pl',
+      't/058_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/058_socket_handle_inheritance.pl b/src/test/recovery/t/058_socket_handle_inheritance.pl
new file mode 100644
--- /dev/null
+++ b/src/test/recovery/t/058_socket_handle_inheritance.pl
@@ -0,0 +1,80 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test that socket handles are not inherited by child processes on Windows.
+#
+# Without the fix, a detached grandchild spawned via COPY TO PROGRAM
+# inherits the listening socket; the kernel's reference counting keeps the
+# port bound after the postmaster dies, and a restart on the same port fails.
+# With the fix, the listening socket is not inherited and the restart succeeds.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+plan skip_all => 'this test exercises Windows socket handle inheritance'
+  if $^O ne 'MSWin32';
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $port = $node->port;
+my $data_dir = $node->data_dir;
+my $marker_file = "$data_dir/copy_marker.txt";
+my $pidfile     = "$data_dir/copy_grandchild_pid.txt";
+unlink $marker_file, $pidfile;
+
+my $copy_cmd =
+    q{powershell -NoProfile -Command "}
+  . qq{Set-Content -Path '$marker_file' -Value 'started' -Encoding ASCII; }
+  . qq{\$p = Start-Process -FilePath powershell -ArgumentList '-NoProfile','-Command','Start-Sleep 300' -WindowStyle Hidden -PassThru; }
+  . qq{\$p.Id | Out-File -FilePath '$pidfile' -Encoding ASCII; }
+  . q{exit 0"}
+  . q{};
+
+$node->safe_psql(
+	'postgres',
+	qq{COPY (SELECT 1) TO PROGRAM '$copy_cmd';});
+
+my $grandchild_pid;
+ok( PostgreSQL::Test::Utils::poll_execute(
+		sub { -e $marker_file && -e $pidfile; }
+	),
+	'detached grandchild spawned by backend COPY TO PROGRAM'
+);
+
+{
+	open my $fh, '<', $pidfile or die "could not open $pidfile: $!";
+	$grandchild_pid = <$fh>;
+	close $fh;
+	chomp $grandchild_pid if defined $grandchild_pid;
+}
+diag("grandchild pid: $grandchild_pid") if defined $grandchild_pid;
+
+$node->kill9;
+sleep 0.5;
+
+my $restarted = $node->start(fail_ok => 1);
+ok($restarted, "postmaster restarted on port $port after crash with detached grandchild running");
+
+if (!$restarted)
+{
+	diag("restart failed - listening port was not freed; ");
+	diag("this indicates the detached grandchild inherited the listening socket");
+}
+
+$node->stop('immediate');
+if (defined $grandchild_pid && $grandchild_pid =~ /^\d+$/)
+{
+	system(
+		qq{powershell -NoProfile -Command "Stop-Process -Id $grandchild_pid -Force -ErrorAction SilentlyContinue" 2>$null}
+	);
+}
+unlink $marker_file, $pidfile;
+
+done_testing();
+

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-test-relocation.diff, git apply patch-test-relocation.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. Apply-check and diff-check confirm structural validity. Targeted-test cannot run: the test is Windows-only (plan skip_all on non-MSWin32) and the build host is Linux.
  • Reason: Windows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
  • Tested patch SHA-256: 106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

Test example: status=provided

  • Vehicle: tap
  • Files: src/test/recovery/t/058_socket_handle_inheritance.pl
  • Oracle: safe_psql returns promptly (no 120s hang). The detached grandchild is still running after safe_psql returns (verified by marker + PID files).
  • Baseline result: Old test: safe_psql hangs ~120s or times out. New test: returns immediately.
  • Patched result: New test returns promptly; grandchild running in background.

---

PAT-3

— MINOR — tests — src/bin/pg_ctl/t/005_socket_handle_inheritance.pl:125-133

Mechanism: 2>nul is cmd.exe redirection syntax used inside powershell -Command strings. PowerShell uses 2>$null. The construct is fragile and confusing.

Failure mode: Misleading/fragile error suppression; may not apply as intended on all Perl/build configurations.

Verification status: verified Verification method: source-analysis Verification evidence: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl:125,133 (staged): 2>nul is cmd.exe syntax. PowerShell-native suppression is 2>$null or -ErrorAction SilentlyContinue.

Repair instruction:

  • Files: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl, src/test/recovery/t/058_socket_handle_inheritance.pl, src/test/recovery/meson.build, src/bin/pg_ctl/meson.build
  • Locations: Cleanup command in the new test
  • Exact edits: Use PowerShell-native 2>$null in the cleanup command, or rely on -ErrorAction SilentlyContinue already present. The relocated test uses 2>$null.
  • Intended postcondition: No cmd.exe-specific 2>nul syntax in PowerShell invocations.

Code example: status=provided, relationship=illustrative

perl
system(qq{powershell -NoProfile -Command "Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue" 2>$null});

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

diff
diff --git a/src/bin/pg_ctl/meson.build b/src/bin/pg_ctl/meson.build
index 69fa7a28427..e7468116b7 100644
--- a/src/bin/pg_ctl/meson.build
+++ b/src/bin/pg_ctl/meson.build
@@ -27,7 +27,6 @@ tests += {
       't/002_status.pl',
       't/003_promote.pl',
       't/004_logrotate.pl',
-      't/005_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl b/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
deleted file mode 100644
--- a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (c) 2025, PostgreSQL Global Development Group
-
-# Test that socket handles are not inherited by child processes on Windows.
-#
-# Without the fix, child processes spawned via COPY TO PROGRAM inherit socket
-# handles from the backend. Windows reference counting prevents these sockets
-# from being freed when the postmaster exits, leaving the port bound to a dead
-# process (a "zombie" binding). This test verifies that after killing the
-# postmaster while a child process is still running, the listening port is
-# immediately freed rather than remaining in a zombie state.
-
-use strict;
-use warnings;
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-use Time::HiRes qw(sleep);
-
-# This test is Windows-specific
-if ($^O ne 'MSWin32')
-{
-	plan skip_all => 'test is specific to Windows socket handle inheritance';
-}
-
-my $node = PostgreSQL::Test::Cluster->new('main');
-$node->init;
-$node->start;
-
-# Get the port number for verification
-my $port = $node->port;
-
-# Spawn a long-running child process via COPY TO PROGRAM that will outlive
-# the postmaster. Without the fix, this child inherits socket handles.
-my $marker_file = $node->data_dir . '/ps_marker.txt';
-unlink $marker_file if -e $marker_file;
-
-$node->safe_psql(
-	'postgres',
-	qq{\\copy (select 1) to program 'powershell -Command "echo marker > $marker_file; Start-Sleep 120"'}
-);
-
-# Wait for PowerShell to spawn
-my $ps_spawned = 0;
-for (my $i = 0; $i < 100; $i++)
-{
-	if (-e $marker_file)
-	{
-		$ps_spawned = 1;
-		last;
-	}
-	sleep 0.1;
-}
-
-ok($ps_spawned, 'child process spawned successfully');
-
-# Stop the postmaster (simulates a crash), leaving the child process running.
-$node->stop('immediate');
-sleep 0.5;
-
-# Verify that the listening port is freed immediately. With the bug, the port
-# remains bound to the dead postmaster PID because the child process inherited
-# the socket handles. With the fix, the port is freed because socket handles
-# were not inherited.
-my $netstat_output = `netstat -ano | findstr ":$port.*LISTENING"`;
-
-if ($netstat_output)
-{
-	fail('listening port remains bound after postmaster exit (zombie port)');
-	diag("Port is still bound - socket handles were inherited by child process");
-	diag("netstat output:\n$netstat_output");
-
-	if ($netstat_output =~ /LISTENING\s+(\d+)/)
-	{
-		my $bound_pid = $1;
-		my $process_name = get_process_name($bound_pid);
-
-		if ($process_name eq 'unknown' || $process_name eq '')
-		{
-			diag("Port bound to dead process (PID $bound_pid) - zombie binding detected");
-		}
-		else
-		{
-			diag("Port bound to: $process_name (PID $bound_pid)");
-		}
-	}
-}
-else
-{
-	pass('listening port freed immediately after postmaster exit');
-}
-
-# Additional verification: Confirm the port is actually available for binding.
-# This tests the real-world scenario that matters to users.
-my $can_bind = test_port_available($port);
-ok($can_bind, "port $port is available for new connections");
-
-# Cleanup
-cleanup_powershell_processes();
-unlink $marker_file if -e $marker_file;
-
-done_testing();
-
-# Test if port can actually be bound
-sub test_port_available
-{
-	my ($port) = @_;
-
-	use Socket;
-
-	socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or return 0;
-	setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
-
-	my $addr = sockaddr_in($port, INADDR_ANY);
-	my $result = bind($sock, $addr);
-	close($sock);
-	
-	return $result ? 1 : 0;
-}
-
-# Get process name by PID
-sub get_process_name
-{
-	my ($pid) = @_;
-	my $name = `powershell -Command "(Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName" 2>nul`;
-	chomp $name;
-	return $name || 'unknown';
-}
-
-# Clean up test child processes
-sub cleanup_powershell_processes
-{
-	system('powershell -Command "Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {$_.Id -ne $PID} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul');
-}
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 123456789ab..23456789abc 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -68,6 +68,7 @@
       't/056_standby_snapshot_export.pl',
       't/057_snapshot_commit_race.pl',
+      't/058_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/058_socket_handle_inheritance.pl b/src/test/recovery/t/058_socket_handle_inheritance.pl
new file mode 100644
--- /dev/null
+++ b/src/test/recovery/t/058_socket_handle_inheritance.pl
@@ -0,0 +1,80 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test that socket handles are not inherited by child processes on Windows.
+#
+# Without the fix, a detached grandchild spawned via COPY TO PROGRAM
+# inherits the listening socket; the kernel's reference counting keeps the
+# port bound after the postmaster dies, and a restart on the same port fails.
+# With the fix, the listening socket is not inherited and the restart succeeds.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+plan skip_all => 'this test exercises Windows socket handle inheritance'
+  if $^O ne 'MSWin32';
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $port = $node->port;
+my $data_dir = $node->data_dir;
+my $marker_file = "$data_dir/copy_marker.txt";
+my $pidfile     = "$data_dir/copy_grandchild_pid.txt";
+unlink $marker_file, $pidfile;
+
+my $copy_cmd =
+    q{powershell -NoProfile -Command "}
+  . qq{Set-Content -Path '$marker_file' -Value 'started' -Encoding ASCII; }
+  . qq{\$p = Start-Process -FilePath powershell -ArgumentList '-NoProfile','-Command','Start-Sleep 300' -WindowStyle Hidden -PassThru; }
+  . qq{\$p.Id | Out-File -FilePath '$pidfile' -Encoding ASCII; }
+  . q{exit 0"}
+  . q{};
+
+$node->safe_psql(
+	'postgres',
+	qq{COPY (SELECT 1) TO PROGRAM '$copy_cmd';});
+
+my $grandchild_pid;
+ok( PostgreSQL::Test::Utils::poll_execute(
+		sub { -e $marker_file && -e $pidfile; }
+	),
+	'detached grandchild spawned by backend COPY TO PROGRAM'
+);
+
+{
+	open my $fh, '<', $pidfile or die "could not open $pidfile: $!";
+	$grandchild_pid = <$fh>;
+	close $fh;
+	chomp $grandchild_pid if defined $grandchild_pid;
+}
+diag("grandchild pid: $grandchild_pid") if defined $grandchild_pid;
+
+$node->kill9;
+sleep 0.5;
+
+my $restarted = $node->start(fail_ok => 1);
+ok($restarted, "postmaster restarted on port $port after crash with detached grandchild running");
+
+if (!$restarted)
+{
+	diag("restart failed - listening port was not freed; ");
+	diag("this indicates the detached grandchild inherited the listening socket");
+}
+
+$node->stop('immediate');
+if (defined $grandchild_pid && $grandchild_pid =~ /^\d+$/)
+{
+	system(
+		qq{powershell -NoProfile -Command "Stop-Process -Id $grandchild_pid -Force -ErrorAction SilentlyContinue" 2>$null}
+	);
+}
+unlink $marker_file, $pidfile;
+
+done_testing();
+

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-test-relocation.diff, git apply patch-test-relocation.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. Apply-check and diff-check confirm structural validity. Targeted-test cannot run: the test is Windows-only (plan skip_all on non-MSWin32) and the build host is Linux.
  • Reason: Windows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
  • Tested patch SHA-256: 106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

Test example: status=provided

  • Vehicle: tap
  • Files: src/test/recovery/t/058_socket_handle_inheritance.pl
  • Oracle: Cleanup command uses PowerShell-native error suppression.
  • Baseline result: Old test uses 2>nul (cmd.exe syntax).
  • Patched result: New test uses 2>$null (PowerShell-native).

---

PAT-4

— NIT — tests — src/bin/pg_ctl/t/005_socket_handle_inheritance.pl:134

Mechanism: The staged test file has no trailing newline at end of file (last byte is } (0x7d)). PostgreSQL convention requires a final newline.

Failure mode: Style/lint: missing trailing newline.

Verification status: verified Verification method: source-analysis Verification evidence: git diff --cached shows \ No newline at end of file for the new test file. Last byte is 0x7d (}).

Repair instruction:

  • Files: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl, src/test/recovery/t/058_socket_handle_inheritance.pl, src/test/recovery/meson.build, src/bin/pg_ctl/meson.build
  • Locations: End of file
  • Exact edits: Ensure the new test file ends with a trailing newline after the final done_testing(); call.
  • Intended postcondition: File ends with a newline byte.

Code example: status=provided, relationship=exact-postimage

perl
done_testing();

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

diff
diff --git a/src/bin/pg_ctl/meson.build b/src/bin/pg_ctl/meson.build
index 69fa7a28427..e7468116b7 100644
--- a/src/bin/pg_ctl/meson.build
+++ b/src/bin/pg_ctl/meson.build
@@ -27,7 +27,6 @@ tests += {
       't/002_status.pl',
       't/003_promote.pl',
       't/004_logrotate.pl',
-      't/005_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl b/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
deleted file mode 100644
--- a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (c) 2025, PostgreSQL Global Development Group
-
-# Test that socket handles are not inherited by child processes on Windows.
-#
-# Without the fix, child processes spawned via COPY TO PROGRAM inherit socket
-# handles from the backend. Windows reference counting prevents these sockets
-# from being freed when the postmaster exits, leaving the port bound to a dead
-# process (a "zombie" binding). This test verifies that after killing the
-# postmaster while a child process is still running, the listening port is
-# immediately freed rather than remaining in a zombie state.
-
-use strict;
-use warnings;
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-use Time::HiRes qw(sleep);
-
-# This test is Windows-specific
-if ($^O ne 'MSWin32')
-{
-	plan skip_all => 'test is specific to Windows socket handle inheritance';
-}
-
-my $node = PostgreSQL::Test::Cluster->new('main');
-$node->init;
-$node->start;
-
-# Get the port number for verification
-my $port = $node->port;
-
-# Spawn a long-running child process via COPY TO PROGRAM that will outlive
-# the postmaster. Without the fix, this child inherits socket handles.
-my $marker_file = $node->data_dir . '/ps_marker.txt';
-unlink $marker_file if -e $marker_file;
-
-$node->safe_psql(
-	'postgres',
-	qq{\\copy (select 1) to program 'powershell -Command "echo marker > $marker_file; Start-Sleep 120"'}
-);
-
-# Wait for PowerShell to spawn
-my $ps_spawned = 0;
-for (my $i = 0; $i < 100; $i++)
-{
-	if (-e $marker_file)
-	{
-		$ps_spawned = 1;
-		last;
-	}
-	sleep 0.1;
-}
-
-ok($ps_spawned, 'child process spawned successfully');
-
-# Stop the postmaster (simulates a crash), leaving the child process running.
-$node->stop('immediate');
-sleep 0.5;
-
-# Verify that the listening port is freed immediately. With the bug, the port
-# remains bound to the dead postmaster PID because the child process inherited
-# the socket handles. With the fix, the port is freed because socket handles
-# were not inherited.
-my $netstat_output = `netstat -ano | findstr ":$port.*LISTENING"`;
-
-if ($netstat_output)
-{
-	fail('listening port remains bound after postmaster exit (zombie port)');
-	diag("Port is still bound - socket handles were inherited by child process");
-	diag("netstat output:\n$netstat_output");
-
-	if ($netstat_output =~ /LISTENING\s+(\d+)/)
-	{
-		my $bound_pid = $1;
-		my $process_name = get_process_name($bound_pid);
-
-		if ($process_name eq 'unknown' || $process_name eq '')
-		{
-			diag("Port bound to dead process (PID $bound_pid) - zombie binding detected");
-		}
-		else
-		{
-			diag("Port bound to: $process_name (PID $bound_pid)");
-		}
-	}
-}
-else
-{
-	pass('listening port freed immediately after postmaster exit');
-}
-
-# Additional verification: Confirm the port is actually available for binding.
-# This tests the real-world scenario that matters to users.
-my $can_bind = test_port_available($port);
-ok($can_bind, "port $port is available for new connections");
-
-# Cleanup
-cleanup_powershell_processes();
-unlink $marker_file if -e $marker_file;
-
-done_testing();
-
-# Test if port can actually be bound
-sub test_port_available
-{
-	my ($port) = @_;
-
-	use Socket;
-
-	socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or return 0;
-	setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
-
-	my $addr = sockaddr_in($port, INADDR_ANY);
-	my $result = bind($sock, $addr);
-	close($sock);
-	
-	return $result ? 1 : 0;
-}
-
-# Get process name by PID
-sub get_process_name
-{
-	my ($pid) = @_;
-	my $name = `powershell -Command "(Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName" 2>nul`;
-	chomp $name;
-	return $name || 'unknown';
-}
-
-# Clean up test child processes
-sub cleanup_powershell_processes
-{
-	system('powershell -Command "Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {$_.Id -ne $PID} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul');
-}
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 123456789ab..23456789abc 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -68,6 +68,7 @@
       't/056_standby_snapshot_export.pl',
       't/057_snapshot_commit_race.pl',
+      't/058_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/058_socket_handle_inheritance.pl b/src/test/recovery/t/058_socket_handle_inheritance.pl
new file mode 100644
--- /dev/null
+++ b/src/test/recovery/t/058_socket_handle_inheritance.pl
@@ -0,0 +1,80 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test that socket handles are not inherited by child processes on Windows.
+#
+# Without the fix, a detached grandchild spawned via COPY TO PROGRAM
+# inherits the listening socket; the kernel's reference counting keeps the
+# port bound after the postmaster dies, and a restart on the same port fails.
+# With the fix, the listening socket is not inherited and the restart succeeds.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+plan skip_all => 'this test exercises Windows socket handle inheritance'
+  if $^O ne 'MSWin32';
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $port = $node->port;
+my $data_dir = $node->data_dir;
+my $marker_file = "$data_dir/copy_marker.txt";
+my $pidfile     = "$data_dir/copy_grandchild_pid.txt";
+unlink $marker_file, $pidfile;
+
+my $copy_cmd =
+    q{powershell -NoProfile -Command "}
+  . qq{Set-Content -Path '$marker_file' -Value 'started' -Encoding ASCII; }
+  . qq{\$p = Start-Process -FilePath powershell -ArgumentList '-NoProfile','-Command','Start-Sleep 300' -WindowStyle Hidden -PassThru; }
+  . qq{\$p.Id | Out-File -FilePath '$pidfile' -Encoding ASCII; }
+  . q{exit 0"}
+  . q{};
+
+$node->safe_psql(
+	'postgres',
+	qq{COPY (SELECT 1) TO PROGRAM '$copy_cmd';});
+
+my $grandchild_pid;
+ok( PostgreSQL::Test::Utils::poll_execute(
+		sub { -e $marker_file && -e $pidfile; }
+	),
+	'detached grandchild spawned by backend COPY TO PROGRAM'
+);
+
+{
+	open my $fh, '<', $pidfile or die "could not open $pidfile: $!";
+	$grandchild_pid = <$fh>;
+	close $fh;
+	chomp $grandchild_pid if defined $grandchild_pid;
+}
+diag("grandchild pid: $grandchild_pid") if defined $grandchild_pid;
+
+$node->kill9;
+sleep 0.5;
+
+my $restarted = $node->start(fail_ok => 1);
+ok($restarted, "postmaster restarted on port $port after crash with detached grandchild running");
+
+if (!$restarted)
+{
+	diag("restart failed - listening port was not freed; ");
+	diag("this indicates the detached grandchild inherited the listening socket");
+}
+
+$node->stop('immediate');
+if (defined $grandchild_pid && $grandchild_pid =~ /^\d+$/)
+{
+	system(
+		qq{powershell -NoProfile -Command "Stop-Process -Id $grandchild_pid -Force -ErrorAction SilentlyContinue" 2>$null}
+	);
+}
+unlink $marker_file, $pidfile;
+
+done_testing();
+

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-test-relocation.diff, git apply patch-test-relocation.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. Apply-check and diff-check confirm structural validity. Targeted-test cannot run: the test is Windows-only (plan skip_all on non-MSWin32) and the build host is Linux.
  • Reason: Windows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
  • Tested patch SHA-256: 106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

Test example: status=waived

  • Waiver kind: design-choice
  • Why: A trailing newline is a formatting fix with no test oracle.

---

PAT-5

— MINOR — maintainability — src/bin/pg_ctl/meson.build:30

Mechanism: The test exercises server backend code (COPY TO PROGRAM, socket handle inheritance, BackendInitialize) but is placed under src/bin/pg_ctl/t/ which tests pg_ctl functionality. The test never invokes pg_ctl directly.

Failure mode: Misleading test placement; couples an unrelated test to the pg_ctl binary test target.

Verification status: verified Verification method: source-analysis Verification evidence: grep for 'pg_ctl' in the test file: zero matches. The test imports PostgreSQL::Test::Cluster and uses COPY TO PROGRAM (copyto.c) and backend_startup.c code. Precedent: src/test/recovery/t/022_crash_temp_files.pl for crash/restart behavior tests.

Repair instruction:

  • Files: src/bin/pg_ctl/t/005_socket_handle_inheritance.pl, src/test/recovery/t/058_socket_handle_inheritance.pl, src/test/recovery/meson.build, src/bin/pg_ctl/meson.build
  • Locations: Move test from src/bin/pg_ctl/t/ to src/test/recovery/t/, Register in src/test/recovery/meson.build, Remove from src/bin/pg_ctl/meson.build
  • Exact edits: Delete src/bin/pg_ctl/t/005_socket_handle_inheritance.pl, create src/test/recovery/t/058_socket_handle_inheritance.pl, add t/058 entry to src/test/recovery/meson.build, remove t/005 entry from src/bin/pg_ctl/meson.build.
  • Intended postcondition: Test is located under src/test/recovery/ which is the canonical home for crash/restart backend behavior tests.

Code example: status=provided, relationship=exact-postimage

meson
      't/057_snapshot_commit_race.pl',
      't/058_socket_handle_inheritance.pl',

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

diff
diff --git a/src/bin/pg_ctl/meson.build b/src/bin/pg_ctl/meson.build
index 69fa7a28427..e7468116b7 100644
--- a/src/bin/pg_ctl/meson.build
+++ b/src/bin/pg_ctl/meson.build
@@ -27,7 +27,6 @@ tests += {
       't/002_status.pl',
       't/003_promote.pl',
       't/004_logrotate.pl',
-      't/005_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl b/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
deleted file mode 100644
--- a/src/bin/pg_ctl/t/005_socket_handle_inheritance.pl
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (c) 2025, PostgreSQL Global Development Group
-
-# Test that socket handles are not inherited by child processes on Windows.
-#
-# Without the fix, child processes spawned via COPY TO PROGRAM inherit socket
-# handles from the backend. Windows reference counting prevents these sockets
-# from being freed when the postmaster exits, leaving the port bound to a dead
-# process (a "zombie" binding). This test verifies that after killing the
-# postmaster while a child process is still running, the listening port is
-# immediately freed rather than remaining in a zombie state.
-
-use strict;
-use warnings;
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-use Time::HiRes qw(sleep);
-
-# This test is Windows-specific
-if ($^O ne 'MSWin32')
-{
-	plan skip_all => 'test is specific to Windows socket handle inheritance';
-}
-
-my $node = PostgreSQL::Test::Cluster->new('main');
-$node->init;
-$node->start;
-
-# Get the port number for verification
-my $port = $node->port;
-
-# Spawn a long-running child process via COPY TO PROGRAM that will outlive
-# the postmaster. Without the fix, this child inherits socket handles.
-my $marker_file = $node->data_dir . '/ps_marker.txt';
-unlink $marker_file if -e $marker_file;
-
-$node->safe_psql(
-	'postgres',
-	qq{\\copy (select 1) to program 'powershell -Command "echo marker > $marker_file; Start-Sleep 120"'}
-);
-
-# Wait for PowerShell to spawn
-my $ps_spawned = 0;
-for (my $i = 0; $i < 100; $i++)
-{
-	if (-e $marker_file)
-	{
-		$ps_spawned = 1;
-		last;
-	}
-	sleep 0.1;
-}
-
-ok($ps_spawned, 'child process spawned successfully');
-
-# Stop the postmaster (simulates a crash), leaving the child process running.
-$node->stop('immediate');
-sleep 0.5;
-
-# Verify that the listening port is freed immediately. With the bug, the port
-# remains bound to the dead postmaster PID because the child process inherited
-# the socket handles. With the fix, the port is freed because socket handles
-# were not inherited.
-my $netstat_output = `netstat -ano | findstr ":$port.*LISTENING"`;
-
-if ($netstat_output)
-{
-	fail('listening port remains bound after postmaster exit (zombie port)');
-	diag("Port is still bound - socket handles were inherited by child process");
-	diag("netstat output:\n$netstat_output");
-
-	if ($netstat_output =~ /LISTENING\s+(\d+)/)
-	{
-		my $bound_pid = $1;
-		my $process_name = get_process_name($bound_pid);
-
-		if ($process_name eq 'unknown' || $process_name eq '')
-		{
-			diag("Port bound to dead process (PID $bound_pid) - zombie binding detected");
-		}
-		else
-		{
-			diag("Port bound to: $process_name (PID $bound_pid)");
-		}
-	}
-}
-else
-{
-	pass('listening port freed immediately after postmaster exit');
-}
-
-# Additional verification: Confirm the port is actually available for binding.
-# This tests the real-world scenario that matters to users.
-my $can_bind = test_port_available($port);
-ok($can_bind, "port $port is available for new connections");
-
-# Cleanup
-cleanup_powershell_processes();
-unlink $marker_file if -e $marker_file;
-
-done_testing();
-
-# Test if port can actually be bound
-sub test_port_available
-{
-	my ($port) = @_;
-
-	use Socket;
-
-	socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or return 0;
-	setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
-
-	my $addr = sockaddr_in($port, INADDR_ANY);
-	my $result = bind($sock, $addr);
-	close($sock);
-	
-	return $result ? 1 : 0;
-}
-
-# Get process name by PID
-sub get_process_name
-{
-	my ($pid) = @_;
-	my $name = `powershell -Command "(Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName" 2>nul`;
-	chomp $name;
-	return $name || 'unknown';
-}
-
-# Clean up test child processes
-sub cleanup_powershell_processes
-{
-	system('powershell -Command "Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {$_.Id -ne $PID} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul');
-}
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 123456789ab..23456789abc 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -68,6 +68,7 @@
       't/056_standby_snapshot_export.pl',
       't/057_snapshot_commit_race.pl',
+      't/058_socket_handle_inheritance.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/058_socket_handle_inheritance.pl b/src/test/recovery/t/058_socket_handle_inheritance.pl
new file mode 100644
--- /dev/null
+++ b/src/test/recovery/t/058_socket_handle_inheritance.pl
@@ -0,0 +1,80 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test that socket handles are not inherited by child processes on Windows.
+#
+# Without the fix, a detached grandchild spawned via COPY TO PROGRAM
+# inherits the listening socket; the kernel's reference counting keeps the
+# port bound after the postmaster dies, and a restart on the same port fails.
+# With the fix, the listening socket is not inherited and the restart succeeds.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+plan skip_all => 'this test exercises Windows socket handle inheritance'
+  if $^O ne 'MSWin32';
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $port = $node->port;
+my $data_dir = $node->data_dir;
+my $marker_file = "$data_dir/copy_marker.txt";
+my $pidfile     = "$data_dir/copy_grandchild_pid.txt";
+unlink $marker_file, $pidfile;
+
+my $copy_cmd =
+    q{powershell -NoProfile -Command "}
+  . qq{Set-Content -Path '$marker_file' -Value 'started' -Encoding ASCII; }
+  . qq{\$p = Start-Process -FilePath powershell -ArgumentList '-NoProfile','-Command','Start-Sleep 300' -WindowStyle Hidden -PassThru; }
+  . qq{\$p.Id | Out-File -FilePath '$pidfile' -Encoding ASCII; }
+  . q{exit 0"}
+  . q{};
+
+$node->safe_psql(
+	'postgres',
+	qq{COPY (SELECT 1) TO PROGRAM '$copy_cmd';});
+
+my $grandchild_pid;
+ok( PostgreSQL::Test::Utils::poll_execute(
+		sub { -e $marker_file && -e $pidfile; }
+	),
+	'detached grandchild spawned by backend COPY TO PROGRAM'
+);
+
+{
+	open my $fh, '<', $pidfile or die "could not open $pidfile: $!";
+	$grandchild_pid = <$fh>;
+	close $fh;
+	chomp $grandchild_pid if defined $grandchild_pid;
+}
+diag("grandchild pid: $grandchild_pid") if defined $grandchild_pid;
+
+$node->kill9;
+sleep 0.5;
+
+my $restarted = $node->start(fail_ok => 1);
+ok($restarted, "postmaster restarted on port $port after crash with detached grandchild running");
+
+if (!$restarted)
+{
+	diag("restart failed - listening port was not freed; ");
+	diag("this indicates the detached grandchild inherited the listening socket");
+}
+
+$node->stop('immediate');
+if (defined $grandchild_pid && $grandchild_pid =~ /^\d+$/)
+{
+	system(
+		qq{powershell -NoProfile -Command "Stop-Process -Id $grandchild_pid -Force -ErrorAction SilentlyContinue" 2>$null}
+	);
+}
+unlink $marker_file, $pidfile;
+
+done_testing();
+

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-test-relocation.diff, git apply patch-test-relocation.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. Apply-check and diff-check confirm structural validity. Targeted-test cannot run: the test is Windows-only (plan skip_all on non-MSWin32) and the build host is Linux.
  • Reason: Windows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
  • Tested patch SHA-256: 106853b5d84ca67a1b11b163c37c8065d82e3a58e95f30f531388ee722c04ea1

Test example: status=waived

  • Waiver kind: design-choice
  • Why: Test placement is a structural decision with no separate test oracle.

---

PAT-6

— MINOR — security — src/backend/tcop/backend_startup.c:186-189

Mechanism: SetHandleInformation failure logs WARNING and continues with an inheritable socket (fail-open). The Unix equivalent (fcntl F_SETFD FD_CLOEXEC in pqcomm.c:301-302) uses elog(FATAL), failing closed.

Failure mode: If SetHandleInformation fails, the socket remains inheritable, silently re-introducing the zombie-port bug. A COPY TO PROGRAM child could then inherit the client socket and read the backend-client wire stream.

Verification status: verified Verification method: source-analysis Verification evidence: src/backend/tcop/backend_startup.c:186-189 (staged): ereport(WARNING,...). src/backend/libpq/pqcomm.c:298-303 (Unix): elog(FATAL, 'fcntl(F_SETFD) failed on socket: %m'). Asymmetric hardening: Unix fails closed, Windows fails open.

Repair instruction:

  • Files: src/backend/tcop/backend_startup.c
  • Locations: Lines 186-189 (staged) — the SetHandleInformation error handling
  • Exact edits: Change ereport(WARNING,...) to ereport(FATAL,...) to match the Unix elog(FATAL) behavior in pqcomm.c:302. Also expand the comment to cite the read_inheritable_socket mechanism (addresses PAT-8).
  • Intended postcondition: SetHandleInformation failure causes backend FATAL exit, matching Unix fcntl(F_SETFD) behavior. No fail-open path.

Code example: status=provided, relationship=exact-postimage

c
#ifdef WIN32
	/*
	 * On Windows, the client socket inherited from the postmaster becomes
	 * inheritable again in this process (read_inheritable_socket recreates it
	 * via WSASocket(FROM_PROTOCOL_INFO, ..., 0) with dwFlags=0, which yields
	 * an inheritable handle). Prevent child processes spawned by this backend
	 * from inheriting it, mirroring the Unix fcntl(F_SETFD, FD_CLOEXEC) in
	 * pqcomm.c. Fail hard like the Unix path does: continuing with an
	 * inheritable socket would reintroduce the zombie-port bug.
	 */
	if (!SetHandleInformation((HANDLE) port->sock, HANDLE_FLAG_INHERIT, 0))
		ereport(FATAL,
				(errmsg_internal("could not disable socket handle inheritance: error code %lu",
								 GetLastError())));
#endif

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=b0998e8f58b53e2585dd4060c8743561d3a133aeb4fa3843a7e7693e020c889f

diff
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 912ad7dc957..7dc3cb137d1 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -177,6 +177,22 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
 	port = MyProcPort = pq_init(client_sock);
 	MemoryContextSwitchTo(oldcontext);
 
+#ifdef WIN32
+	/*
+	 * On Windows, the client socket inherited from the postmaster becomes
+	 * inheritable again in this process (read_inheritable_socket recreates it
+	 * via WSASocket(FROM_PROTOCOL_INFO, ..., 0) with dwFlags=0, which yields
+	 * an inheritable handle). Prevent child processes spawned by this backend
+	 * from inheriting it, mirroring the Unix fcntl(F_SETFD, FD_CLOEXEC) in
+	 * pqcomm.c. Fail hard like the Unix path does: continuing with an
+	 * inheritable socket would reintroduce the zombie-port bug.
+	 */
+	if (!SetHandleInformation((HANDLE) port->sock, HANDLE_FLAG_INHERIT, 0))
+		ereport(FATAL,
+				(errmsg_internal("could not disable socket handle inheritance: error code %lu",
+								 GetLastError())));
+#endif
+
 	whereToSendOutput = DestRemote; /* now safe to ereport to client */
 
 	/* set these to empty in case they are needed before we set them up */

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-backend-startup-fatal-and-comment.diff, git apply patch-backend-startup-fatal-and-comment.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. The applied diff matches the patch content exactly. The change replaces WARNING with FATAL and expands the comment.
  • Reason: WIN32-guarded code (#ifdef WIN32) not compilable on Linux build host. Compile and targeted-test cannot run.
  • Tested patch SHA-256: b0998e8f58b53e2585dd4060c8743561d3a133aeb4fa3843a7e7693e020c889f

Test example: status=provided

  • Vehicle: tap
  • Files: src/test/recovery/t/058_socket_handle_inheritance.pl
  • Oracle: With FATAL: if SetHandleInformation fails, backend exits (fail-closed). With WARNING: backend continues with inheritable socket (fail-open).
  • Baseline result: On Windows with WARNING: fail-open if SetHandleInformation fails. Not runnable on Linux.
  • Patched result: On Windows with FATAL: fail-closed, matching Unix behavior.

---

PAT-7

— MINOR — security — src/backend/port/win32/socket.c:341-359

Mechanism: pgwin32_accept calls WSAAccept which does not accept WSA_FLAG_NO_HANDLE_INHERIT. The accepted socket may be inheritable depending on the LSP. No explicit SetHandleInformation is called on the accepted socket in the postmaster.

Failure mode: Accepted client sockets may remain inheritable in the postmaster, allowing them to leak to sibling backends via CreateProcess(bInheritHandles=TRUE) before BackendInitialize clears the backend's copy.

Verification status: verified Verification method: source-analysis Verification evidence: src/backend/port/win32/socket.c:341-359 (pgwin32_accept calls WSAAccept with no flag control). MSDN: WSAAccept does not propagate WSA_FLAG_NO_HANDLE_INHERIT. src/backend/postmaster/launch_backend.c:466 (CreateProcess bInheritHandles=TRUE). Thomas Munro raised this on-list (accept4() suggestion, thread 626963, 2025-11-07).

Repair instruction:

  • Files: src/backend/port/win32/socket.c
  • Locations: pgwin32_accept function, after WSAAccept returns successfully
  • Exact edits: Add SetHandleInformation((HANDLE) rs, HANDLE_FLAG_INHERIT, 0) after WSAAccept succeeds. On failure, close the socket and return INVALID_SOCKET.
  • Intended postcondition: All accepted sockets are explicitly non-inheritable, regardless of LSP behavior.

Code example: status=provided, relationship=illustrative

c
	rs = WSAAccept(s, addr, addrlen, NULL, 0);
	if (rs == INVALID_SOCKET)
	{
		TranslateSocketError();
		return INVALID_SOCKET;
	}

	if (!SetHandleInformation((HANDLE) rs, HANDLE_FLAG_INHERIT, 0))
	{
		TranslateSocketError();
		closesocket(rs);
		return INVALID_SOCKET;
	}

	return rs;

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=c1e43960afcda0455e0bdd506636143855b5f32061e6b65bef0cbf94b268a82b

diff
diff --git a/src/backend/port/win32/socket.c b/src/backend/port/win32/socket.c
index e16fd85ddd4..0f56b2ed272 100644
--- a/src/backend/port/win32/socket.c
+++ b/src/backend/port/win32/socket.c
@@ -285,7 +285,11 @@ pgwin32_waitforsinglesocket(SOCKET s, int what, int timeout)
 }
 
 /*
- * Create a socket, setting it to overlapped and non-blocking
+ * Create a socket, setting it to overlapped, non-blocking, and non-inheritable.
+ *
+ * We must prevent child processes from inheriting socket handles. Otherwise,
+ * the kernel's reference counting means listening sockets can stay bound even
+ * after postmaster exit, preventing restart.
  */
 SOCKET
 pgwin32_socket(int af, int type, int protocol)
@@ -293,7 +297,8 @@ pgwin32_socket(int af, int type, int protocol)
 	SOCKET		s;
 	unsigned long on = 1;
 
-	s = WSASocket(af, type, protocol, NULL, 0, WSA_FLAG_OVERLAPPED);
+	s = WSASocket(af, type, protocol, NULL, 0,
+				  WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);
 	if (s == INVALID_SOCKET)
 	{
 		TranslateSocketError();
@@ -350,6 +355,21 @@ pgwin32_accept(SOCKET s, struct sockaddr *addr, int *addrlen)
 		TranslateSocketError();
 		return INVALID_SOCKET;
 	}
+
+	/*
+	 * WSAAccept does not accept WSA_FLAG_NO_HANDLE_INHERIT and does not
+	 * guarantee that the accepted socket is non-inheritable (the result may
+	 * depend on the installed Layered Service Provider). Explicitly clear the
+	 * inherit flag so that child processes spawned by this backend (e.g., via
+	 * COPY TO PROGRAM) do not inherit the client socket.
+	 */
+	if (!SetHandleInformation((HANDLE) rs, HANDLE_FLAG_INHERIT, 0))
+	{
+		TranslateSocketError();
+		closesocket(rs);
+		return INVALID_SOCKET;
+	}
+
 	return rs;
 }

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-socket-combined.diff, git apply patch-socket-combined.diff
  • Evidence: Patch applies cleanly to HEAD 6f3bdadaadc. Includes both the pgwin32_socket WSA_FLAG_NO_HANDLE_INHERIT change and the pgwin32_accept SetHandleInformation fix.
  • Reason: WIN32-guarded code (src/backend/port/win32/socket.c) not compilable on Linux build host. Compile and targeted-test cannot run.
  • Tested patch SHA-256: c1e43960afcda0455e0bdd506636143855b5f32061e6b65bef0cbf94b268a82b

Test example: status=provided

  • Vehicle: tap
  • Files: src/test/recovery/t/058_socket_handle_inheritance.pl
  • Oracle: With the accept fix: accepted sockets are explicitly non-inheritable. Without: accepted sockets may be inheritable depending on LSP.
  • Baseline result: On Windows without accept fix: accepted socket may be inheritable depending on LSP. Not runnable on Linux.
  • Patched result: On Windows with accept fix: accepted socket explicitly non-inheritable.

---

PAT-8

— NIT — correctness — src/backend/tcop/backend_startup.c:182-185

Mechanism: The comment says the socket 'becomes inheritable again in this process' but does not explain why: read_inheritable_socket recreates it via WSASocket(FROM_PROTOCOL_INFO,...,0) with dwFlags=0, yielding an inheritable handle.

Failure mode: Reduced readability for future maintainers; the mechanism is non-obvious.

Verification status: verified Verification method: source-analysis Verification evidence: src/backend/postmaster/launch_backend.c:851-856 (read_inheritable_socket uses WSASocket(FROM_PROTOCOL_INFO,...,0) with dwFlags=0). The staged comment at backend_startup.c:182-185 is correct but imprecise.

Repair instruction:

  • Files: src/backend/tcop/backend_startup.c
  • Locations: Lines 182-185 (staged comment)
  • Exact edits: Expand the comment to cite the read_inheritable_socket mechanism (WSASocket FROM_PROTOCOL_INFO with dwFlags=0) and reference the Unix fcntl(F_SETFD, FD_CLOEXEC) in pqcomm.c. Combined with PAT-6 fix.
  • Intended postcondition: Comment explains why the socket becomes inheritable again, not just that it does.

Code example: status=provided, relationship=exact-postimage

c
#ifdef WIN32
	/*
	 * On Windows, the client socket inherited from the postmaster becomes
	 * inheritable again in this process (read_inheritable_socket recreates it
	 * via WSASocket(FROM_PROTOCOL_INFO, ..., 0) with dwFlags=0, which yields
	 * an inheritable handle). Prevent child processes spawned by this backend
	 * from inheriting it, mirroring the Unix fcntl(F_SETFD, FD_CLOEXEC) in
	 * pqcomm.c. Fail hard like the Unix path does: continuing with an
	 * inheritable socket would reintroduce the zombie-port bug.
	 */
	if (!SetHandleInformation((HANDLE) port->sock, HANDLE_FLAG_INHERIT, 0))
		ereport(FATAL,
				(errmsg_internal("could not disable socket handle inheritance: error code %lu",
								 GetLastError())));
#endif

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=b0998e8f58b53e2585dd4060c8743561d3a133aeb4fa3843a7e7693e020c889f

diff
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 912ad7dc957..7dc3cb137d1 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -177,6 +177,22 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
 	port = MyProcPort = pq_init(client_sock);
 	MemoryContextSwitchTo(oldcontext);
 
+#ifdef WIN32
+	/*
+	 * On Windows, the client socket inherited from the postmaster becomes
+	 * inheritable again in this process (read_inheritable_socket recreates it
+	 * via WSASocket(FROM_PROTOCOL_INFO, ..., 0) with dwFlags=0, which yields
+	 * an inheritable handle). Prevent child processes spawned by this backend
+	 * from inheriting it, mirroring the Unix fcntl(F_SETFD, FD_CLOEXEC) in
+	 * pqcomm.c. Fail hard like the Unix path does: continuing with an
+	 * inheritable socket would reintroduce the zombie-port bug.
+	 */
+	if (!SetHandleInformation((HANDLE) port->sock, HANDLE_FLAG_INHERIT, 0))
+		ereport(FATAL,
+				(errmsg_internal("could not disable socket handle inheritance: error code %lu",
+								 GetLastError())));
+#endif
+
 	whereToSendOutput = DestRemote; /* now safe to ereport to client */
 
 	/* set these to empty in case they are needed before we set them up */

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-backend-startup-fatal-and-comment.diff, git apply patch-backend-startup-fatal-and-comment.diff
  • Evidence: Patch applies cleanly to HEAD. The comment is expanded to cite read_inheritable_socket and the dwFlags=0 mechanism. Combined with PAT-6 FATAL fix.
  • Reason: WIN32-guarded code not compilable on Linux build host.
  • Tested patch SHA-256: b0998e8f58b53e2585dd4060c8743561d3a133aeb4fa3843a7e7693e020c889f

Test example: status=waived

  • Waiver kind: design-choice
  • Why: A comment improvement has no behavioral test oracle.

---

PAT-9

— NIT — docs — src/backend/port/win32/socket.c:300-301

Mechanism: WSA_FLAG_NO_HANDLE_INHERIT requires Windows Vista+ (_WIN32_WINNT >= 0x0600). PostgreSQL's floor is 0x0A00 (Windows 10), so the flag is always available. The comment does not document this availability floor.

Failure mode: Future maintainers may worry about compatibility; no docs note explains the floor is safe.

Verification status: verified Verification method: source-analysis Verification evidence: src/include/port/win32.h:16-20 (_WIN32_WINNT = 0x0A00). WSA_FLAG_NO_HANDLE_INHERIT (0x80) available since Windows Vista (0x0600). Floor is far exceeded.

Repair instruction:

  • Files: src/backend/port/win32/socket.c
  • Locations: Comment above pgwin32_socket (staged comment at lines 287-293)
  • Exact edits: Add a note to the comment documenting that WSA_FLAG_NO_HANDLE_INHERIT requires Vista+ (_WIN32_WINNT >= 0x0600) and our floor is 0x0A00 (Windows 10), so the flag is always available.
  • Intended postcondition: Comment documents the availability floor of WSA_FLAG_NO_HANDLE_INHERIT.

Code example: status=provided, relationship=illustrative

c
/*
 * Create a socket, setting it to overlapped, non-blocking, and non-inheritable.
 *
 * We must prevent child processes from inheriting socket handles. Otherwise,
 * the kernel's reference counting means listening sockets can stay bound even
 * after postmaster exit, preventing restart.
 *
 * WSA_FLAG_NO_HANDLE_INHERIT is available since Windows Vista (_WIN32_WINNT
 * >= 0x0600); our minimum target is _WIN32_WINNT 0x0A00 (Windows 10), so the
 * flag is always available.
 */
SOCKET
pgwin32_socket(int af, int type, int protocol)
{
	SOCKET		s;
	unsigned long on = 1;

	s = WSASocket(af, type, protocol, NULL, 0,
				  WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);

Patch: status=provided, format=unified-diff, applies_to_head=6f3bdadaadc4692050ca20dff53d3890088b9272, sha256=ae0a5754d9fc174488441f63becb0860dfc69270c5a3e5eb3af42d7bdbda64c1

diff
diff --git a/src/backend/port/win32/socket.c b/src/backend/port/win32/socket.c
index e16fd85ddd4..6787b159cff 100644
--- a/src/backend/port/win32/socket.c
+++ b/src/backend/port/win32/socket.c
@@ -285,7 +285,15 @@ pgwin32_waitforsinglesocket(SOCKET s, int what, int timeout)
 }
 
 /*
- * Create a socket, setting it to overlapped and non-blocking
+ * Create a socket, setting it to overlapped, non-blocking, and non-inheritable.
+ *
+ * We must prevent child processes from inheriting socket handles. Otherwise,
+ * the kernel's reference counting means listening sockets can stay bound even
+ * after postmaster exit, preventing restart.
+ *
+ * WSA_FLAG_NO_HANDLE_INHERIT is available since Windows Vista (_WIN32_WINNT
+ * >= 0x0600); our minimum target is _WIN32_WINNT 0x0A00 (Windows 10), so the
+ * flag is always available.
  */
 SOCKET
 pgwin32_socket(int af, int type, int protocol)
@@ -293,7 +301,8 @@ pgwin32_socket(int af, int type, int protocol)
 	SOCKET		s;
 	unsigned long on = 1;
 
-	s = WSASocket(af, type, protocol, NULL, 0, WSA_FLAG_OVERLAPPED);
+	s = WSASocket(af, type, protocol, NULL, 0,
+				  WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);
 	if (s == INVALID_SOCKET)
 	{
 		TranslateSocketError();

Repair validation: status=not-run

  • Methods: apply-check, diff-check
  • Commands: git apply --check patch-socket-docs-floor.diff, git apply patch-socket-docs-floor.diff
  • Evidence: Patch applies cleanly to HEAD. The comment now documents the Vista+ availability floor and confirms the 0x0A00 floor is safe.
  • Reason: WIN32-guarded code not compilable on Linux build host.
  • Tested patch SHA-256: ae0a5754d9fc174488441f63becb0860dfc69270c5a3e5eb3af42d7bdbda64c1

Test example: status=waived

  • Waiver kind: design-choice
  • Why: A documentation comment has no behavioral test oracle.

---

Verification Summary

All 9 findings were verified via source analysis. 0 refuted. 0 cannot-verify.

FindingVerificationMethod
PAT-1verifiedsource-analysis
PAT-2verifiedsource-analysis
PAT-3verifiedsource-analysis
PAT-4verifiedsource-analysis
PAT-5verifiedsource-analysis
PAT-6verifiedsource-analysis
PAT-7verifiedsource-analysis
PAT-8verifiedsource-analysis
PAT-9verifiedsource-analysis

No runtime verification was possible on the Linux build host because all changed C code is #ifdef WIN32-guarded and the TAP test is Windows-only (plan skip_all on non-MSWin32). Patches were validated with git apply --check (apply-check) and git diff comparison (diff-check) in a disposable worktree at HEAD.

Repair Validation Summary

FindingValidation StatusMethodsReason
PAT-1not-runapply-check, diff-checkWindows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
PAT-2not-runapply-check, diff-checkWindows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
PAT-3not-runapply-check, diff-checkWindows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
PAT-4not-runapply-check, diff-checkWindows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
PAT-5not-runapply-check, diff-checkWindows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Targeted-test cannot run on Linux build host. Only apply-check and diff-check were performed.
PAT-6not-runapply-check, diff-checkWIN32-guarded code (#ifdef WIN32) not compilable on Linux build host. Compile and targeted-test cannot run.
PAT-7not-runapply-check, diff-checkWIN32-guarded code (src/backend/port/win32/socket.c) not compilable on Linux build host. Compile and targeted-test cannot run.
PAT-8not-runapply-check, diff-checkWIN32-guarded code not compilable on Linux build host.
PAT-9not-runapply-check, diff-checkWIN32-guarded code not compilable on Linux build host.

Test-related findings (PAT-1 through PAT-5): apply-check and diff-check passed. Targeted-test could not run (Windows-only test on Linux host). C-code findings (PAT-6 through PAT-9): apply-check and diff-check passed. Compile could not run (WIN32-guarded code on Linux host).

Changed-Code Coverage

Coverage requirement: not-applicable

Reason: All changed C implementation code is WIN32-guarded and not compiled on this Linux build host. src/backend/port/win32/socket.c is only compiled when host_system == 'windows' (src/backend/port/meson.build:28). The #ifdef WIN32 block in backend_startup.c is preprocessed out on Linux. The TAP test is Windows-only. No instrumentable changed implementation paths exist on this build host; a fresh coverage epoch cannot produce gcov/gcovr counters for the changed code.

Non-instrumented changes:

PathReason
src/backend/port/win32/socket.cWIN32-only file; not compiled on Linux build host (src/backend/port/meson.build:28 guards subdir('win32') behind host_system == 'windows'). Changed lines in pgwin32_socket (WSA_FLAG_NO_HANDLE_INHERIT) and pgwin32_accept (if addressed) are non-instrumentable here.
src/backend/tcop/backend_startup.cThe changed lines (180-190) are inside #ifdef WIN32, preprocessed out on Linux. The rest of BackendInitialize is unchanged. No instrumentable changed lines on this host.
src/bin/pg_ctl/t/005_socket_handle_inheritance.plWindows-only TAP test (plan skip_all if $^O ne 'MSWin32'). Skips immediately on Linux, producing a vacuous pass with no coverage data.
src/bin/pg_ctl/meson.buildBuild system metadata file; not instrumentable code.

Residual limitation: A Windows CI runner (cirrus-windows/bf-windows) is required to measure coverage of the patched WIN32 code paths. Coverage measurement is deferred to that environment.

pgsql-hackers Thread Ledger

Status: reconciled Ledger report slug: commitfest-6207-review-handoff-thread-ledger Thread: 626963 — [PATCH] Fix socket handle inheritance on Windows Participants: Bryan Green, Thomas Munro

Message chronology

#DateSenderKindSummary
12025-11-06T05:06:25+00:00Bryan GreenpatchInitial patch submission. Reproduces zombie port binding: psql \copy to long-running powershell, kil...
22025-11-06T05:34:00+00:00Bryan GreenpatchCorrection: wrong file extension on the initial patch; resends with correct patch....
32025-11-07T01:03:06+00:00Thomas MunroreviewEndorses WSA_FLAG_NO_HANDLE_INHERIT change. Suggests SOCK_CLOEXEC / socket_set_cloexec() helper to h...
42025-11-07T01:35:29+00:00Bryan Greenauthor-replyAgrees to implement SOCK_CLOEXEC abstraction and socket_set_cloexec() helper. Says 'My bug fix will ...
52025-11-07T01:40:00+00:00Thomas Munrocommitter-reply+1 Thanks for working on all this stuff. Cool. No objections, green light....

Finding dispositions

FindingDispositionOn-list?Rationale
PAT-6already-raisedYesThomas Munro raised the socket_set_cloexec() harmonization and the FATAL-vs-WARN...
PAT-7already-raisedYesThomas Munro noted the accept4()-equivalent need for accepted sockets in the sam...
PAT-1newNoThe test-exercises-wrong-code-path defect (\copy is client-side) was not raised ...
PAT-2newNoThe safe_psql hang / pclose-blocks defect was not raised on-list....
PAT-3newNoThe 2>nul-vs-2>$null syntax defect was not raised on-list....
PAT-4newNoMissing trailing newline was not raised on-list....
PAT-5newNoTest placement under pg_ctl was not raised on-list....
PAT-8newNoComment improvement suggestion was not raised on-list....
PAT-9newNoDocs-floor note was not raised on-list....

Key thread outcomes:

Security Summary

No BLOCKER or MAJOR security issues. The patch correctly closes the Windows client-socket-inheritance gap (Unix FD_CLOEXEC equivalent). Remaining security items:

Aggregated Test Plan

  1. Replace src/bin/pg_ctl/t/005_socket_handle_inheritance.pl with src/test/recovery/t/058_socket_handle_inheritance.pl (PAT-1/PAT-2/PAT-3/PAT-4/PAT-5).
  2. The new test uses server-side COPY ... TO PROGRAM (not client-side \copy), a detached Start-Process grandchild (not blocking safe_psql), PowerShell-native error suppression, and a trailing newline.
  3. Two-run validation must be performed on a Windows CI runner: apply the test on base (expect restart failure — port stuck) and on HEAD (expect restart success — port freed).
  4. The SetHandleInformation failure path (PAT-6 FATAL) is not deterministically triggerable without an injection point; documented as a follow-up.

Provenance

SeatModelSlug
pg-patch-reviewerglm-5.2:cloudcommitfest-6207-review-handoff-patch-reviewer
pg-security-reviewerglm-5.2:cloudcommitfest-6207-review-handoff-security-reviewer
pg-test-authorglm-5.2:cloudcommitfest-6207-review-handoff-test-author
pg-finding-verifierglm-5.2:cloudcommitfest-6207-review-handoff-verifier
thread-reconcilerglm-5.2:cloudcommitfest-6207-review-handoff-thread-ledger
pg-change-coverage-reviewerN/Anot-applicable (WIN32 code)

Residual Risk

Deterministic Verdict

waiting-on-author

Rationale:

Blocker split: verified=0, unverified=0. No blockers at any severity level.