Skip to content

Commit 1ed9dbf

Browse files
dbgreenhackorum
authored andcommitted
Fix socket handle inheritance on Windows preventing restart
On Windows, socket handles are inheritable by default, causing child processes spawned by backends (e.g., via COPY TO PROGRAM) to inherit socket handles. Windows reference counting then prevents sockets from being freed when the owning process exits, leading to "Address already in use" errors on restart or zombie connections in netstat. Fix by adding WSA_FLAG_NO_HANDLE_INHERIT to socket creation in pgwin32_socket(), and calling SetHandleInformation() in BackendInitialize() to make the inherited client socket non-inheritable before spawning children. The latter is needed because handles passed to child processes become inheritable again on Windows.
1 parent b597835 commit 1ed9dbf

4 files changed

Lines changed: 154 additions & 2 deletions

File tree

src/backend/port/win32/socket.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,20 @@ pgwin32_waitforsinglesocket(SOCKET s, int what, int timeout)
285285
}
286286

287287
/*
288-
* Create a socket, setting it to overlapped and non-blocking
288+
* Create a socket, setting it to overlapped, non-blocking, and non-inheritable.
289+
*
290+
* We must prevent child processes from inheriting socket handles. Otherwise,
291+
* the kernel's reference counting means listening sockets can stay bound even
292+
* after postmaster exit, preventing restart.
289293
*/
290294
SOCKET
291295
pgwin32_socket(int af, int type, int protocol)
292296
{
293297
SOCKET s;
294298
unsigned long on = 1;
295299

296-
s = WSASocket(af, type, protocol, NULL, 0, WSA_FLAG_OVERLAPPED);
300+
s = WSASocket(af, type, protocol, NULL, 0,
301+
WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);
297302
if (s == INVALID_SOCKET)
298303
{
299304
TranslateSocketError();

src/backend/tcop/backend_startup.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
177177
port = MyProcPort = pq_init(client_sock);
178178
MemoryContextSwitchTo(oldcontext);
179179

180+
#ifdef WIN32
181+
/*
182+
* On Windows, the client socket inherited from the postmaster becomes
183+
* inheritable again in this process. Prevent child processes spawned
184+
* by this backend from inheriting it.
185+
*/
186+
if (!SetHandleInformation((HANDLE) port->sock, HANDLE_FLAG_INHERIT, 0))
187+
ereport(WARNING,
188+
(errmsg_internal("could not disable socket handle inheritance: error code %lu",
189+
GetLastError())));
190+
#endif
191+
180192
whereToSendOutput = DestRemote; /* now safe to ereport to client */
181193

182194
/* set these to empty in case they are needed before we set them up */

src/bin/pg_ctl/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ tests += {
2727
't/002_status.pl',
2828
't/003_promote.pl',
2929
't/004_logrotate.pl',
30+
't/005_socket_handle_inheritance.pl',
3031
],
3132
},
3233
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright (c) 2025, PostgreSQL Global Development Group
2+
3+
# Test that socket handles are not inherited by child processes on Windows.
4+
#
5+
# Without the fix, child processes spawned via COPY TO PROGRAM inherit socket
6+
# handles from the backend. Windows reference counting prevents these sockets
7+
# from being freed when the postmaster exits, leaving the port bound to a dead
8+
# process (a "zombie" binding). This test verifies that after killing the
9+
# postmaster while a child process is still running, the listening port is
10+
# immediately freed rather than remaining in a zombie state.
11+
12+
use strict;
13+
use warnings;
14+
15+
use PostgreSQL::Test::Cluster;
16+
use PostgreSQL::Test::Utils;
17+
use Test::More;
18+
use Time::HiRes qw(sleep);
19+
20+
# This test is Windows-specific
21+
if ($^O ne 'MSWin32')
22+
{
23+
plan skip_all => 'test is specific to Windows socket handle inheritance';
24+
}
25+
26+
my $node = PostgreSQL::Test::Cluster->new('main');
27+
$node->init;
28+
$node->start;
29+
30+
# Get the port number for verification
31+
my $port = $node->port;
32+
33+
# Spawn a long-running child process via COPY TO PROGRAM that will outlive
34+
# the postmaster. Without the fix, this child inherits socket handles.
35+
my $marker_file = $node->data_dir . '/ps_marker.txt';
36+
unlink $marker_file if -e $marker_file;
37+
38+
$node->safe_psql(
39+
'postgres',
40+
qq{\\copy (select 1) to program 'powershell -Command "echo marker > $marker_file; Start-Sleep 120"'}
41+
);
42+
43+
# Wait for PowerShell to spawn
44+
my $ps_spawned = 0;
45+
for (my $i = 0; $i < 100; $i++)
46+
{
47+
if (-e $marker_file)
48+
{
49+
$ps_spawned = 1;
50+
last;
51+
}
52+
sleep 0.1;
53+
}
54+
55+
ok($ps_spawned, 'child process spawned successfully');
56+
57+
# Stop the postmaster (simulates a crash), leaving the child process running.
58+
$node->stop('immediate');
59+
sleep 0.5;
60+
61+
# Verify that the listening port is freed immediately. With the bug, the port
62+
# remains bound to the dead postmaster PID because the child process inherited
63+
# the socket handles. With the fix, the port is freed because socket handles
64+
# were not inherited.
65+
my $netstat_output = `netstat -ano | findstr ":$port.*LISTENING"`;
66+
67+
if ($netstat_output)
68+
{
69+
fail('listening port remains bound after postmaster exit (zombie port)');
70+
diag("Port is still bound - socket handles were inherited by child process");
71+
diag("netstat output:\n$netstat_output");
72+
73+
if ($netstat_output =~ /LISTENING\s+(\d+)/)
74+
{
75+
my $bound_pid = $1;
76+
my $process_name = get_process_name($bound_pid);
77+
78+
if ($process_name eq 'unknown' || $process_name eq '')
79+
{
80+
diag("Port bound to dead process (PID $bound_pid) - zombie binding detected");
81+
}
82+
else
83+
{
84+
diag("Port bound to: $process_name (PID $bound_pid)");
85+
}
86+
}
87+
}
88+
else
89+
{
90+
pass('listening port freed immediately after postmaster exit');
91+
}
92+
93+
# Additional verification: Confirm the port is actually available for binding.
94+
# This tests the real-world scenario that matters to users.
95+
my $can_bind = test_port_available($port);
96+
ok($can_bind, "port $port is available for new connections");
97+
98+
# Cleanup
99+
cleanup_powershell_processes();
100+
unlink $marker_file if -e $marker_file;
101+
102+
done_testing();
103+
104+
# Test if port can actually be bound
105+
sub test_port_available
106+
{
107+
my ($port) = @_;
108+
109+
use Socket;
110+
111+
socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or return 0;
112+
setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
113+
114+
my $addr = sockaddr_in($port, INADDR_ANY);
115+
my $result = bind($sock, $addr);
116+
close($sock);
117+
118+
return $result ? 1 : 0;
119+
}
120+
121+
# Get process name by PID
122+
sub get_process_name
123+
{
124+
my ($pid) = @_;
125+
my $name = `powershell -Command "(Get-Process -Id $pid -ErrorAction SilentlyContinue).ProcessName" 2>nul`;
126+
chomp $name;
127+
return $name || 'unknown';
128+
}
129+
130+
# Clean up test child processes
131+
sub cleanup_powershell_processes
132+
{
133+
system('powershell -Command "Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {$_.Id -ne $PID} | Stop-Process -Force -ErrorAction SilentlyContinue" 2>nul');
134+
}

0 commit comments

Comments
 (0)