On 11/1/2026 2:03 PM, Bryan Green wrote:
> On 11/6/2025 7:59 PM, Thomas Munro wrote:
>> On Fri, Nov 7, 2025 at 2:35 PM Bryan Green <[email protected]> wrote:
>>> If you agree, and I think you do, I will implement the SOCK_CLOEXEC
>>> abstraction and the socket_set_cloexec helper for this. My bug fix will
>>> be the first user.
>>
>> +1
>>
>> Thanks for working on all this stuff.
>>
>>> I also need to handle the handles for shared memory and pipes. I will
>>> probably just write those up tonight and share for review and discussion.
>>
>> Cool.
>
> After a long wait... here's v2! It's two patches now. 0002 is the
> shared memory fix I said would follow back in November.
>
> 0001 does sockets the way Thomas suggested. Backend code on Windows
> now gets a SOCK_CLOEXEC, which pgwin32_socket() maps to
> WSA_FLAG_NO_HANDLE_INHERIT, so you ask for a non-inheritable socket
> the same way you do on Unix instead of v1's blanket change. The listen
> sockets ask for it.
>
> The macro is backend-only, since libpq passes SOCK_CLOEXEC whenever
> it's defined and Winsock would reject it. I left accept4() out. It
> only matters once we have threads.
>
> Regarding 0002, the postmaster made its shared memory handle
> inheritable on purpose, so children could use the same handle value.
> That also gave it to everything the children run. If the server dies
> while one of those is still running, the next postmaster fails with
> "pre-existing shared memory block is still in use". Now the
> postmaster's handle isn't inheritable, and save_backend_variables()
> duplicates it into each child, the way the signal pipe already is.
> Neither copy in the child is inheritable. That covers every child, not
> just backends.
>
> The test (src/test/postmaster/t/005_handle_inheritance.pl) uses
> Harrison's idea of running the program with server-side COPY TO
> PROGRAM. v1's test used \copy, which made the program psql's child, so
> it couldn't have caught anything. The program is a small Perl script
> that outlives an immediate stop. 0001 checks that the client sees its
> connection drop and that the port can be bound again-- 0002 adds a
> restart. All fail on master and pass with the patches. Tested with
> MSVC on Windows 11 (regress, recovery, isolation, postmaster, pg_ctl,
> libpq).
>
> Harrison, thanks for looking at this. I didn't need force_initdb here.
> If you saw failures without it, what were they?
>
Obviously...here are the patches.
--
Bryan Green
EDB: https://www.enterprisedb.com
From a0969f7f6cb037900fc43437d319c690f01fa0d8 Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Sun, 1 Nov 2026 10:56:33 -0600
Subject: [PATCH v2 1/2] Fix socket handle inheritance on Windows.
On Windows, socket handles are inheritable by default, and the server
starts its child processes with handle inheritance enabled. Programs
run by a backend, via COPY TO PROGRAM, archive_command and so on,
therefore inherit the postmaster's listen sockets and the backend's
client socket. If the server exits while one of them is still running,
the client's connection stays open and the port can't be bound again
until the program exits.
Fix by adding SOCK_CLOEXEC for backend code on Windows, which
pgwin32_socket() maps to WSA_FLAG_NO_HANDLE_INHERIT, and using it for
the listen sockets. Also add socket_set_cloexec(), and use it in
pq_init() on all platforms in place of fcntl(). Windows needs that too,
because the socket handle read_inheritable_socket() creates in the
backend is inheritable.
Suggested-by: Thomas Munro <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/libpq/pqcomm.c | 22 +++--
src/backend/port/win32/socket.c | 12 ++-
src/include/port.h | 3 +-
src/include/port/win32_port.h | 6 ++
src/port/noblock.c | 21 ++++-
src/test/postmaster/meson.build | 1 +
.../postmaster/t/005_handle_inheritance.pl | 93 +++++++++++++++++++
7 files changed, 145 insertions(+), 13 deletions(-)
create mode 100644 src/test/postmaster/t/005_handle_inheritance.pl
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 3704d12100..4d2ccb56f7 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -295,12 +295,9 @@ pq_init(ClientSocket *client_sock)
(errmsg("could not set socket to nonblocking
mode: %m")));
#endif
-#ifndef WIN32
-
/* Don't give the socket to any subprograms we execute. */
- if (fcntl(port->sock, F_SETFD, FD_CLOEXEC) < 0)
- elog(FATAL, "fcntl(F_SETFD) failed on socket: %m");
-#endif
+ if (!socket_set_cloexec(port->sock))
+ elog(FATAL, "could not set socket to close-on-exec mode: %m");
FeBeWaitSet = CreateWaitEventSet(NULL, FeBeWaitSetNEvents);
socket_pos = AddWaitEventToSet(FeBeWaitSet, WL_SOCKET_WRITEABLE,
@@ -536,7 +533,13 @@ ListenServerPort(int family, const char *hostName,
unsigned short portNumber,
addrDesc = addrBuf;
}
- if ((fd = socket(addr->ai_family, SOCK_STREAM, 0)) ==
PGINVALID_SOCKET)
+ /* Don't give the listen socket to any subprograms we execute.
*/
+#ifdef SOCK_CLOEXEC
+ fd = socket(addr->ai_family, SOCK_STREAM | SOCK_CLOEXEC, 0);
+#else
+ fd = socket(addr->ai_family, SOCK_STREAM, 0);
+#endif
+ if (fd == PGINVALID_SOCKET)
{
ereport(LOG,
(errcode_for_socket_access(),
@@ -545,11 +548,12 @@ ListenServerPort(int family, const char *hostName,
unsigned short portNumber,
familyDesc, addrDesc)));
continue;
}
+#ifndef SOCK_CLOEXEC
+ if (!socket_set_cloexec(fd))
+ elog(FATAL, "could not set socket to close-on-exec
mode: %m");
+#endif
#ifndef WIN32
- /* Don't give the listen socket to any subprograms we execute.
*/
- if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0)
- elog(FATAL, "fcntl(F_SETFD) failed on socket: %m");
/*
* Without the SO_REUSEADDR flag, a new postmaster can't be
started
diff --git a/src/backend/port/win32/socket.c b/src/backend/port/win32/socket.c
index 092d41e3f6..7b1cc4a442 100644
--- a/src/backend/port/win32/socket.c
+++ b/src/backend/port/win32/socket.c
@@ -285,15 +285,23 @@ 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 and non-blocking, and to
+ * non-inheritable if type includes SOCK_CLOEXEC
*/
SOCKET
pgwin32_socket(int af, int type, int protocol)
{
SOCKET s;
unsigned long on = 1;
+ DWORD flags = WSA_FLAG_OVERLAPPED;
- s = WSASocket(af, type, protocol, NULL, 0, WSA_FLAG_OVERLAPPED);
+ if (type & SOCK_CLOEXEC)
+ {
+ flags |= WSA_FLAG_NO_HANDLE_INHERIT;
+ type &= ~SOCK_CLOEXEC;
+ }
+
+ s = WSASocket(af, type, protocol, NULL, 0, flags);
if (s == INVALID_SOCKET)
{
TranslateSocketError();
diff --git a/src/include/port.h b/src/include/port.h
index 172acf7d02..0bab9e28ae 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -40,9 +40,10 @@ typedef SOCKET pgsocket;
typedef unsigned int socklen_t;
#endif
-/* non-blocking */
+/* non-blocking, close-on-exec */
extern bool pg_set_noblock(pgsocket sock);
extern bool pg_set_block(pgsocket sock);
+extern bool socket_set_cloexec(pgsocket sock);
/* Portable path handling for Unix/Win32 (in path.c) */
diff --git a/src/include/port/win32_port.h b/src/include/port/win32_port.h
index aff6eaa28f..ecae0cca2e 100644
--- a/src/include/port/win32_port.h
+++ b/src/include/port/win32_port.h
@@ -518,6 +518,12 @@ extern int pgkill(int pid, int sig);
#define recv(s, buf, len, flags) pgwin32_recv(s, buf, len, flags)
#define send(s, buf, len, flags) pgwin32_send(s, buf, len, flags)
+/*
+ * pgwin32_socket() maps this to WSA_FLAG_NO_HANDLE_INHERIT. Frontend code
+ * calls Winsock directly, which would reject it.
+ */
+#define SOCK_CLOEXEC 0x80000
+
extern SOCKET pgwin32_socket(int af, int type, int protocol);
extern int pgwin32_bind(SOCKET s, struct sockaddr *addr, int addrlen);
extern int pgwin32_listen(SOCKET s, int backlog);
diff --git a/src/port/noblock.c b/src/port/noblock.c
index c4a6080990..4e4f0cd0be 100644
--- a/src/port/noblock.c
+++ b/src/port/noblock.c
@@ -1,7 +1,7 @@
/*-------------------------------------------------------------------------
*
* noblock.c
- * set a file descriptor as blocking or non-blocking
+ * set a file descriptor as blocking or non-blocking, or close-on-exec
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -64,3 +64,22 @@ pg_set_block(pgsocket sock)
return (ioctlsocket(sock, FIONBIO, &ioctlsocket_ret) == 0);
#endif
}
+
+/*
+ * Put socket into close-on-exec mode.
+ * Returns true on success, false on failure.
+ */
+bool
+socket_set_cloexec(pgsocket sock)
+{
+#if !defined(WIN32)
+ return (fcntl(sock, F_SETFD, FD_CLOEXEC) != -1);
+#else
+ if (!SetHandleInformation((HANDLE) sock, HANDLE_FLAG_INHERIT, 0))
+ {
+ _dosmaperr(GetLastError());
+ return false;
+ }
+ return true;
+#endif
+}
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
index fa30883b60..af05a9f38f 100644
--- a/src/test/postmaster/meson.build
+++ b/src/test/postmaster/meson.build
@@ -10,6 +10,7 @@ tests += {
't/002_connection_limits.pl',
't/003_start_stop.pl',
't/004_negotiate.pl',
+ 't/005_handle_inheritance.pl',
],
},
}
diff --git a/src/test/postmaster/t/005_handle_inheritance.pl
b/src/test/postmaster/t/005_handle_inheritance.pl
new file mode 100644
index 0000000000..ff79ee1b05
--- /dev/null
+++ b/src/test/postmaster/t/005_handle_inheritance.pl
@@ -0,0 +1,93 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# On Windows, check that a program started by COPY TO PROGRAM doesn't
+# inherit handles that keep the server's resources alive after the server
+# is gone.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+
+if (!$windows_os)
+{
+ plan skip_all => 'handle inheritance is specific to Windows';
+}
+
+my $tempdir = PostgreSQL::Test::Utils::tempdir;
+my $ready = "$tempdir/ready";
+my $release = "$tempdir/release";
+
+# The program records its PID, then waits until we release it.
+my $script = "$tempdir/linger.pl";
+append_to_file(
+ $script, q{
+my ($ready, $release) = @ARGV;
+open(my $fh, '>', "$ready.tmp") or die;
+print $fh $$;
+close($fh);
+rename("$ready.tmp", $ready) or die;
+for (1 .. 3000)
+{
+ last if -e $release;
+ select(undef, undef, undef, 0.1);
+}
+});
+
+# Wait for $cond to become true, or give up after the default timeout.
+sub wait_until
+{
+ my ($cond) = @_;
+
+ for (my $i = 0; $i < 10 * $PostgreSQL::Test::Utils::timeout_default;
$i++)
+ {
+ return 1 if $cond->();
+ usleep(100_000);
+ }
+ return 0;
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $psql = $node->background_psql('postgres');
+$psql->query_until(
+ qr/started/, qq{\\echo started
+COPY (SELECT 1) TO PROGRAM '"$^X" "$script" "$ready" "$release"';
+});
+
+wait_until(sub { -e $ready }) or die "COPY TO PROGRAM did not start";
+my $pid = slurp_file($ready);
+
+# This takes a few seconds: the backend is waiting for the program, so it
+# doesn't see the shutdown signal, and the postmaster has to kill it.
+$node->stop('immediate');
+
+SKIP:
+{
+ skip 'program did not outlive the server', 2 unless kill(0, $pid);
+
+ # The client socket: the client should see its connection drop.
+ $psql->{timeout}->start($PostgreSQL::Test::Utils::timeout_default);
+ ok( pump_until(
+ $psql->{run}, $psql->{timeout}, \$psql->{stderr},
+ qr/server closed the connection unexpectedly|connection
to server was lost/
+ ),
+ 'client connection closed while program is still running');
+
+ # The listen sockets: the port should be free for a new postmaster.
+ ok(PostgreSQL::Test::Cluster::can_bind($node->host, $node->port),
+ 'port is free while program is still running');
+}
+
+append_to_file($release, '');
+wait_until(sub { !kill(0, $pid) }) or die "program did not exit";
+
+# psql has exited by now, having lost its connection, so don't try \q.
+$psql->{run}->finish;
+
+done_testing();
--
2.54.0.windows.1
From 0ca87f840b810e95daf43c1b2bc1b24026c735f7 Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Sun, 1 Nov 2026 11:30:40 -0600
Subject: [PATCH v2 2/2] Fix shared memory handle inheritance on Windows.
The postmaster made its handle to the main shared memory segment
inheritable, so that child processes could use the same handle value.
Programs run by those children, via COPY TO PROGRAM, archive_command
and so on, then inherited it as well. If the server exits while one of
them is still running, the segment stays in use, and a new postmaster
fails with "pre-existing shared memory block is still in use" until the
program exits.
Fix by keeping the postmaster's handle non-inheritable, and duplicating
it into each child in save_backend_variables(), as is already done for
the signal pipe. The child's copies of both are now non-inheritable.
Discussion: https://postgr.es/m/[email protected]
---
src/backend/port/win32_shmem.c | 46 ++++++-------------
src/backend/postmaster/launch_backend.c | 21 +++++++--
.../postmaster/t/005_handle_inheritance.pl | 8 +++-
3 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/src/backend/port/win32_shmem.c b/src/backend/port/win32_shmem.c
index 794e4fcb2a..72a61596d6 100644
--- a/src/backend/port/win32_shmem.c
+++ b/src/backend/port/win32_shmem.c
@@ -209,8 +209,7 @@ PGSharedMemoryCreate(Size size,
{
void *memAddress;
PGShmemHeader *hdr;
- HANDLE hmap,
- hmap2;
+ HANDLE hmap;
char *szShareMem;
int i;
DWORD size_high;
@@ -341,21 +340,6 @@ retry:
free(szShareMem);
- /*
- * Make the handle inheritable
- */
- if (!DuplicateHandle(GetCurrentProcess(), hmap, GetCurrentProcess(),
&hmap2, 0, TRUE, DUPLICATE_SAME_ACCESS))
- ereport(FATAL,
- (errmsg("could not create shared memory
segment: error code %lu", GetLastError()),
- errdetail("Failed system call was
DuplicateHandle.")));
-
- /*
- * Close the old, non-inheritable handle. If this fails we don't really
- * care.
- */
- if (!CloseHandle(hmap))
- elog(LOG, "could not close handle to shared memory: error code
%lu", GetLastError());
-
desiredAccess = FILE_MAP_WRITE | FILE_MAP_READ;
#ifdef FILE_MAP_LARGE_PAGES
@@ -368,7 +352,7 @@ retry:
* Get a pointer to the new shared memory segment. Map the whole segment
* at once, and let the system decide on the initial address.
*/
- memAddress = MapViewOfFileEx(hmap2, desiredAccess, 0, 0, 0, NULL);
+ memAddress = MapViewOfFileEx(hmap, desiredAccess, 0, 0, 0, NULL);
if (!memAddress)
ereport(FATAL,
(errmsg("could not create shared memory
segment: error code %lu", GetLastError()),
@@ -395,10 +379,10 @@ retry:
/* Save info for possible future use */
UsedShmemSegAddr = memAddress;
UsedShmemSegSize = size;
- UsedShmemSegID = hmap2;
+ UsedShmemSegID = hmap;
/* Register on-exit routine to delete the new segment */
- on_shmem_exit(pgwin32_SharedMemoryDelete, PointerGetDatum(hmap2));
+ on_shmem_exit(pgwin32_SharedMemoryDelete, PointerGetDatum(hmap));
*shim = hdr;
@@ -413,12 +397,12 @@ retry:
* PGSharedMemoryReAttach
*
* This is called during startup of a postmaster child process to re-attach to
- * an already existing shared memory segment, using the handle inherited from
- * the postmaster.
+ * an already existing shared memory segment, using the handle the postmaster
+ * duplicated for it.
*
* ShmemProtectiveRegion, UsedShmemSegID and UsedShmemSegAddr are implicit
- * parameters to this routine. The caller must have already restored them to
- * the postmaster's values.
+ * parameters to this routine. The caller must have already restored them
+ * from the backend parameters.
*/
void
PGSharedMemoryReAttach(void)
@@ -465,8 +449,8 @@ PGSharedMemoryReAttach(void)
* after this; make sure that it will be a no-op if called.
*
* ShmemProtectiveRegion, UsedShmemSegID and UsedShmemSegAddr are implicit
- * parameters to this routine. The caller must have already restored them to
- * the postmaster's values.
+ * parameters to this routine. The caller must have already restored them
+ * from the backend parameters.
*/
void
PGSharedMemoryNoReAttach(void)
@@ -482,9 +466,9 @@ PGSharedMemoryNoReAttach(void)
UsedShmemSegAddr = NULL;
/*
- * We *must* close the inherited shmem segment handle, else Windows will
- * consider the existence of this process to mean it can't release the
- * shmem segment yet. We can now use PGSharedMemoryDetach to do that.
+ * We *must* close our shmem segment handle, else Windows will consider
+ * the existence of this process to mean it can't release the shmem
+ * segment yet. We can now use PGSharedMemoryDetach to do that.
*/
PGSharedMemoryDetach();
}
@@ -495,8 +479,8 @@ PGSharedMemoryNoReAttach(void)
* Detach from the shared memory segment, if still attached. This is not
* intended to be called explicitly by the process that originally created the
* segment (it will have an on_shmem_exit callback registered to do that).
- * Rather, this is for subprocesses that have inherited an attachment and want
- * to get rid of it.
+ * Rather, this is for subprocesses that have been given an attachment and
+ * want to get rid of it.
*
* ShmemProtectiveRegion, UsedShmemSegID and UsedShmemSegAddr are implicit
* parameters to this routine.
diff --git a/src/backend/postmaster/launch_backend.c
b/src/backend/postmaster/launch_backend.c
index 093ef87912..6221dc1caf 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -726,8 +726,22 @@ save_backend_variables(BackendParameters *param,
#ifdef WIN32
param->ShmemProtectiveRegion = ShmemProtectiveRegion;
-#endif
+
+ /* Give the child its own handle to the segment, not inheritable */
+ param->UsedShmemSegID = INVALID_HANDLE_VALUE;
+ if (UsedShmemSegID != INVALID_HANDLE_VALUE &&
+ !DuplicateHandle(GetCurrentProcess(), UsedShmemSegID,
+ childProcess,
¶m->UsedShmemSegID,
+ 0, FALSE,
DUPLICATE_SAME_ACCESS))
+ {
+ ereport(LOG,
+ (errmsg_internal("could not duplicate shared
memory handle for child process: error code %lu",
+
GetLastError())));
+ return false;
+ }
+#else
param->UsedShmemSegID = UsedShmemSegID;
+#endif
param->UsedShmemSegAddr = UsedShmemSegAddr;
#ifdef USE_INJECTION_POINTS
@@ -782,7 +796,8 @@ save_backend_variables(BackendParameters *param,
#ifdef WIN32
/*
* Duplicate a handle for usage in a child process, and write the child
- * process instance of the handle to the parameter file.
+ * process instance of the handle to the parameter file. The child's copy
+ * is not inheritable.
*/
static bool
write_duplicated_handle(HANDLE *dest, HANDLE src, HANDLE childProcess)
@@ -794,7 +809,7 @@ write_duplicated_handle(HANDLE *dest, HANDLE src, HANDLE
childProcess)
childProcess,
&hChild,
0,
- TRUE,
+ FALSE,
DUPLICATE_CLOSE_SOURCE |
DUPLICATE_SAME_ACCESS))
{
ereport(LOG,
diff --git a/src/test/postmaster/t/005_handle_inheritance.pl
b/src/test/postmaster/t/005_handle_inheritance.pl
index ff79ee1b05..6bf8143803 100644
--- a/src/test/postmaster/t/005_handle_inheritance.pl
+++ b/src/test/postmaster/t/005_handle_inheritance.pl
@@ -69,7 +69,7 @@ $node->stop('immediate');
SKIP:
{
- skip 'program did not outlive the server', 2 unless kill(0, $pid);
+ skip 'program did not outlive the server', 3 unless kill(0, $pid);
# The client socket: the client should see its connection drop.
$psql->{timeout}->start($PostgreSQL::Test::Utils::timeout_default);
@@ -82,6 +82,12 @@ SKIP:
# The listen sockets: the port should be free for a new postmaster.
ok(PostgreSQL::Test::Cluster::can_bind($node->host, $node->port),
'port is free while program is still running');
+
+ # The shared memory segment: a new postmaster should start. The program
+ # does still hold the old log file as its stderr, so give the new
+ # postmaster another one.
+ $node->rotate_logfile;
+ ok($node->start(fail_ok => 1), 'restart while program is still
running');
}
append_to_file($release, '');
--
2.54.0.windows.1