From a5a5f52a5a0af2c87478e4686665460206ea29d4 Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Thu, 30 Oct 2025 11:14:55 -0600
Subject: [PATCH 1/2] Use Windows Job Objects to prevent orphaned child
 processes (local rebase)

Original proposal: Bryan Green, CF6201 public v2.
Discussion: https://www.postgresql.org/message-id/880214db-ab8c-4b9e-852c-b0f6d90d3f3d@gmail.com

Locally rebased by Harrison Booth for review against recorded
158b6d5109fad9d5f0c30693fa85041a1c621fe1. This is not a new
Bryan revision or an author-endorsed change. The existing local
integration shortens the postmaster call-site comment; original
Job logic, diagnostics and recovery test are preserved. Known
nested-job comment and supervisor-policy claims still need author
review. Apply before the separate anonymous-job supplement.
---
 doc/src/sgml/runtime.sgml                |  21 +++
 src/backend/postmaster/postmaster.c      |   6 +
 src/backend/storage/ipc/Makefile         |   4 +
 src/backend/storage/ipc/meson.build      |   7 +
 src/backend/storage/ipc/pg_job_object.c  | 176 +++++++++++++++++++++++
 src/include/storage/pg_job_object.h      |  35 +++++
 src/test/recovery/t/013_crash_restart.pl | 111 ++++++++++++++
 7 files changed, 360 insertions(+)
 create mode 100644 src/backend/storage/ipc/pg_job_object.c
 create mode 100644 src/include/storage/pg_job_object.h

diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index d9984910cc46c9e5b3a655bda4945580131af161..87e4f4e0a0d6bafca2ff6335caa382c4dba75ba0 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -1520,6 +1520,27 @@ $ <userinput>cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages</userinp
    </para>
 
   </sect2>
+
+  <sect2 id="windows-process-management">
+   <title>Process Management on <systemitem class="osname">Windows</systemitem></title>
+
+   <para>
+    On <systemitem class="osname">Windows</systemitem>,
+    <productname>PostgreSQL</productname> uses Job Objects to
+    manage child processes. If the postmaster exits unexpectedly
+    (due to a crash or external termination), the operating system
+    automatically terminates all backend processes. This prevents
+    orphaned backends that could hold locks or corrupt shared memory.
+   </para>
+
+   <para>
+    In some cases, Job Object creation may fail (for example, when
+    <productname>PostgreSQL</productname> is already running under a
+    job-aware service manager). The server will log a message and continue
+    to run without orphan protection. This is safe but means that backends
+    may need to be manually terminated if the postmaster crashes.
+   </para>
+  </sect2>
  </sect1>
 
 
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index f53aa9c404d5f9027e89817b3bd1b174f9222eff..f4b2bd099fc9fe3bf4abdda769409b0b834bd870 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -113,6 +113,7 @@
 #include "storage/fd.h"
 #include "storage/io_worker.h"
 #include "storage/ipc.h"
+#include "storage/pg_job_object.h"
 #include "storage/pmsignal.h"
 #include "storage/proc.h"
 #include "storage/shmem_internal.h"
@@ -1016,6 +1017,11 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	CreateSharedMemoryAndSemaphores();
 
+#ifdef WIN32
+	/* Configure child cleanup before launching any Windows backends. */
+	pg_create_job_object();
+#endif
+
 	/*
 	 * Estimate number of openable files.  This must happen after setting up
 	 * semaphores, because on some platforms semaphores count as open files.
diff --git a/src/backend/storage/ipc/Makefile b/src/backend/storage/ipc/Makefile
index f71653bbe482e36a57f2cfad93bd6a975c2aef66..fbb69c0d20d50b3eda9869c7879acdc2c2ef9f6c 100644
--- a/src/backend/storage/ipc/Makefile
+++ b/src/backend/storage/ipc/Makefile
@@ -29,4 +29,8 @@ OBJS = \
 	standby.o \
 	waiteventset.o
 
+ifeq ($(PORTNAME), win32)
+OBJS += pg_job_object.o
+endif
+
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/storage/ipc/meson.build b/src/backend/storage/ipc/meson.build
index b8c31e29967dad06a7b991584dc53c2b8a63fd97..eb7c7047e0e4f3d43043af23ef30289fba4b3f50 100644
--- a/src/backend/storage/ipc/meson.build
+++ b/src/backend/storage/ipc/meson.build
@@ -22,3 +22,10 @@ backend_sources += files(
   'waiteventset.c',
 
 )
+
+# Windows-specific files
+if host_system == 'windows'
+  backend_sources += files(
+    'pg_job_object.c',
+  )
+endif
diff --git a/src/backend/storage/ipc/pg_job_object.c b/src/backend/storage/ipc/pg_job_object.c
new file mode 100644
index 0000000000000000000000000000000000000000..4c9bd90f3b3c1515e9e217f31f88d50a94e3e577
--- /dev/null
+++ b/src/backend/storage/ipc/pg_job_object.c
@@ -0,0 +1,176 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_job_object.c
+ *	  Windows Job Object support for preventing orphaned backends
+ *
+ * On Unix, backends can detect when the postmaster dies via getppid().
+ * Windows has no equivalent mechanism. We solve this by using Job Objects,
+ * a Windows kernel feature that groups processes and can automatically
+ * terminate all members when the job handle closes.
+ *
+ * By configuring JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, we ensure that if
+ * the postmaster exits (cleanly or via crash), Windows immediately kills
+ * all backends. This prevents orphaned processes that hold locks and
+ * prevent clean restart.
+ *
+ * The job object handle is stored in a static variable and never explicitly
+ * closed. This is intentional - we rely on Windows closing it automatically
+ * when the postmaster process exits, which triggers the child termination.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/storage/ipc/pg_job_object.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#ifdef WIN32
+
+#include "postmaster/postmaster.h"
+#include "storage/ipc.h"
+#include "storage/pg_job_object.h"
+
+static HANDLE pg_job_object = NULL;
+
+
+/*
+ * pg_create_job_object
+ *
+ * Create job object for this PostgreSQL instance and configure it to
+ * kill all children when the postmaster exits.
+ *
+ * Failure is not fatal - we log a warning and continue. PostgreSQL will
+ * run without orphan protection, which is no worse than current behavior.
+ */
+void
+pg_create_job_object(void)
+{
+	JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit_info;
+	char		job_name[128];
+	DWORD		error;
+
+	snprintf(job_name, sizeof(job_name), "PostgreSQL_Port_%d_PID_%lu",
+			 PostPortNumber, GetCurrentProcessId());
+
+	pg_job_object = CreateJobObjectA(NULL, job_name);
+
+	if (pg_job_object == NULL)
+	{
+		error = GetLastError();
+		ereport(LOG,
+				(errmsg("could not create job object \"%s\": error code %lu",
+						job_name, error),
+				 errdetail("Orphaned process cleanup will not be available.")));
+		return;
+	}
+
+	elog(DEBUG1, "created job object \"%s\"", job_name);
+
+	/*
+	 * Set KILL_ON_JOB_CLOSE. When the job handle closes (either explicit
+	 * close or process termination), all processes in the job are terminated.
+	 *
+	 * This is the critical flag that prevents orphaned backends.
+	 */
+	memset(&limit_info, 0, sizeof(limit_info));
+	limit_info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
+
+	if (!SetInformationJobObject(pg_job_object,
+								 JobObjectExtendedLimitInformation,
+								 &limit_info,
+								 sizeof(limit_info)))
+	{
+		error = GetLastError();
+		ereport(WARNING,
+				(errmsg("could not configure job object: error code %lu", error),
+				 errdetail("Job object created but KILL_ON_JOB_CLOSE not set."),
+				 errhint("Orphaned processes may occur if postmaster crashes.")));
+		CloseHandle(pg_job_object);
+		pg_job_object = NULL;
+		return;
+	}
+
+	if (!AssignProcessToJobObject(pg_job_object, GetCurrentProcess()))
+	{
+		error = GetLastError();
+
+		/*
+		 * ERROR_ACCESS_DENIED means we're already in a job. This can happen
+		 * when PostgreSQL runs under a job-aware supervisor (Windows service
+		 * on older Windows, or any process manager using nested jobs).
+		 *
+		 * On Windows 8+, we could use nested jobs, but for simplicity we just
+		 * skip job creation. The parent job should handle cleanup.
+		 */
+		if (error == ERROR_ACCESS_DENIED)
+		{
+			ereport(LOG,
+					(errmsg("postmaster is already in a job object"),
+					 errdetail("This can occur when PostgreSQL is run under a job-aware supervisor."),
+					 errhint("Automatic orphan cleanup will not be available.")));
+		}
+		else
+		{
+			ereport(WARNING,
+					(errmsg("could not assign postmaster to job object: error code %lu", error)));
+		}
+
+		CloseHandle(pg_job_object);
+		pg_job_object = NULL;
+		return;
+	}
+
+	elog(LOG, "PostgreSQL job object configured successfully - orphaned process prevention enabled");
+}
+
+
+/*
+ * pg_destroy_job_object
+ *
+ * Explicitly close the job object handle. This will trigger KILL_ON_JOB_CLOSE,
+ * terminating all backends.
+ *
+ * Note: In most cases we don't call this - we rely on Windows closing the
+ * handle automatically when the postmaster exits. Explicit close is only
+ * needed if we want to control the exact timing of backend termination.
+ */
+void
+pg_destroy_job_object(void)
+{
+	if (pg_job_object != NULL)
+	{
+		elog(DEBUG1, "closing job object - all child processes will terminate");
+		CloseHandle(pg_job_object);
+		pg_job_object = NULL;
+	}
+}
+
+
+/*
+ * pg_is_in_job_object
+ *
+ * Check if current process is in the PostgreSQL job object.
+ * Used primarily for testing and verification.
+ */
+bool
+pg_is_in_job_object(void)
+{
+	BOOL		in_job = FALSE;
+
+	if (pg_job_object == NULL)
+		return false;
+
+	if (!IsProcessInJob(GetCurrentProcess(), pg_job_object, &in_job))
+	{
+		elog(DEBUG1, "IsProcessInJob failed: error code %lu", GetLastError());
+		return false;
+	}
+
+	return (bool) in_job;
+}
+
+#endif							/* WIN32 */
diff --git a/src/include/storage/pg_job_object.h b/src/include/storage/pg_job_object.h
new file mode 100644
index 0000000000000000000000000000000000000000..67de54be5b71484f6977718b964fb45c2d633082
--- /dev/null
+++ b/src/include/storage/pg_job_object.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_job_object.h
+ *	  Windows Job Object support for preventing orphaned backends
+ *
+ * When the postmaster crashes on Windows, child processes continue running
+ * because Windows has no equivalent to Unix's parent death detection. Job
+ * Objects solve this by allowing the kernel to terminate all children when
+ * the job handle closes (which happens automatically on process exit).
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/storage/pg_job_object.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_JOB_OBJECT_H
+#define PG_JOB_OBJECT_H
+
+#ifdef WIN32
+
+extern void pg_create_job_object(void);
+extern void pg_destroy_job_object(void);
+extern bool pg_is_in_job_object(void);
+
+#else							/* !WIN32 */
+
+#define pg_create_job_object() ((void) 0)
+#define pg_destroy_job_object() ((void) 0)
+#define pg_is_in_job_object() (false)
+
+#endif							/* WIN32 */
+
+#endif							/* PG_JOB_OBJECT_H */
diff --git a/src/test/recovery/t/013_crash_restart.pl b/src/test/recovery/t/013_crash_restart.pl
index 0fde920713e53c1f52e87d4edc15a370803c1309..60834d31c763587e6410d88f14351eea88279d32 100644
--- a/src/test/recovery/t/013_crash_restart.pl
+++ b/src/test/recovery/t/013_crash_restart.pl
@@ -278,4 +278,115 @@ is( $node->safe_psql(
 
 $node->stop();
 
+# Test Windows Job Objects orphan prevention
+if ($windows_os)
+{
+	note "testing Windows Job Object orphan prevention";
+
+	my $jobtest = PostgreSQL::Test::Cluster->new('jobtest');
+	$jobtest->init(allows_streaming => 1);
+	$jobtest->start();
+
+	my $log_contents = slurp_file($jobtest->logfile);
+	like($log_contents, qr/orphaned process prevention enabled/,
+		"Job Objects enabled on startup");
+
+	$jobtest->safe_psql('postgres',
+		q[ALTER SYSTEM SET restart_after_crash = 1;
+		  SELECT pg_reload_conf();]);
+
+	my ($backend_stdin, $backend_stdout, $backend_stderr) = ('', '', '');
+	my $backend = IPC::Run::start(
+		[
+			'psql', '--no-psqlrc', '--quiet', '--no-align', '--tuples-only',
+			'--file', '-', '--dbname' => $jobtest->connstr('postgres')
+		],
+		'<'  => \$backend_stdin,
+		'>'  => \$backend_stdout,
+		'2>' => \$backend_stderr,
+		$psql_timeout);
+
+	$backend_stdin .= q[
+BEGIN;
+CREATE TABLE jobtest(id int);
+SELECT pg_backend_pid();
+];
+	ok( pump_until(
+			$backend, $psql_timeout,
+			\$backend_stdout, qr/[[:digit:]]+[\r\n]$/m),
+		'acquired backend pid for job object test');
+	my $backend_pid = $backend_stdout;
+	chomp($backend_pid);
+	$backend_stdout = '';
+
+	note "backend PID: $backend_pid";
+
+	my $postmaster_pidfile = $jobtest->data_dir . '/postmaster.pid';
+	open(my $pidfh, '<', $postmaster_pidfile)
+		or die "cannot open postmaster.pid: $!";
+	my $postmaster_pid = <$pidfh>;
+	close($pidfh);
+	chomp($postmaster_pid);
+
+	note "postmaster PID: $postmaster_pid";
+	ok($postmaster_pid =~ /^\d+$/ && $postmaster_pid > 0,
+		"got valid postmaster PID: $postmaster_pid");
+
+	note "killing postmaster PID $postmaster_pid";
+	my $ret = PostgreSQL::Test::Utils::system_log('pg_ctl', 'kill', 'KILL',
+		$postmaster_pid);
+	is($ret, 0, "killed postmaster with pg_ctl kill KILL");
+
+	eval { $backend->finish; };
+
+	note "waiting for postmaster process to exit";
+	my $postmaster_gone = 0;
+	for (my $i = 0; $i < 30; $i++)
+	{
+		my $check = `tasklist /FI "PID eq $postmaster_pid" /NH 2>&1`;
+		if ($check !~ /postgres\.exe/)
+		{
+			$postmaster_gone = 1;
+			note "postmaster exited after " . (($i + 1) * 100) . "ms";
+			last;
+		}
+		select(undef, undef, undef, 0.1);
+	}
+
+	ok($postmaster_gone, "postmaster process exited");
+
+	$jobtest->{_pid} = undef;
+
+	unlink($postmaster_pidfile);
+
+	# THE REAL TEST: Can we restart without conflicts?
+	# If orphans exist, they'll block the port or shared memory
+	note "attempting restart (will fail if orphans present)";
+
+	my $restart_ok = 0;
+	eval {
+		$jobtest->start();
+		$restart_ok = 1;
+	};
+
+	if (!$restart_ok) {
+		fail("restart failed - orphans may be blocking resources: $@");
+	} else {
+		pass("restart succeeded - no orphans blocking port or shared memory");
+
+		eval {
+			my $result = $jobtest->safe_psql('postgres', 'SELECT 1');
+			is($result, '1', 'server functional after restart');
+		};
+		if ($@) {
+			note "query after restart failed: $@";
+			fail("query after restart failed");
+		}
+	}
+
+	note "stopping test cluster";
+	eval { $jobtest->stop('immediate'); };
+}
+
+
 done_testing();
-- 
2.53.0.windows.2

