From a52a89204e7664d4a60e6f07fca92ae2ecf1c840 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Thu, 20 Aug 2026 00:43:06 +0000
Subject: [PATCH] Fix postmaster wedge when startup process crashes during
 crash restart

Commit 9b43e6793b0f removed the PM_STARTUP shortcut that made the
postmaster exit when the startup process died unexpectedly, relying on
the regular crash path (HandleChildCrash()) to also cover this case, so
that the other children that may now be running during PM_STARTUP are
cleaned up rather than orphaned.

However, HandleChildCrash() returns without doing anything when
FatalError is already set, which is exactly the state when a relaunched
startup process crashes during a crash restart before WAL redo has
started (receipt of PMSIGNAL_RECOVERY_STARTED is what clears
FatalError).  In that case nothing moves the state machine off
PM_STARTUP: StartupStatus is set to STARTUP_CRASHED, but the only place
consulting that flag (PM_NO_CHILDREN handling) is never reached, and
the postmaster keeps relaunching the checkpointer, the background
writer, and the IO workers forever, without writing anything to the
log.  In the report, a Windows postmaster whose console had gone away
-- so that every child it created died with 0xC0000142 during process
initialization -- burned a full CPU core silently respawning children,
while pg_ctl still reported the server as running and shutdown requests
were not honored.

Fix by restoring the historical give-up behavior for this case: when
the startup process crashes while FatalError is still set, log the
child exit, signal the remaining children, and proceed towards
PM_NO_CHILDREN, where the existing STARTUP_CRASHED check makes the
postmaster exit.  Unlike before 9b43e6793b0f, this goes through the
regular fatal-error path instead of exiting directly, so no children
are orphaned.

Bug: #19623
Reported-by: KUAN-TING KUO <m0935388420@gmail.com>
Author: Zexin Li <lizi.openmind@gmail.com>
Discussion: https://postgr.es/m/19623-f9bd331940be1273@postgresql.org
---
 src/backend/postmaster/postmaster.c | 33 ++++++++++++++++++++++++-----
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 1711743f1a..2f0580699d 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -432,6 +432,7 @@ static void process_pm_shutdown_request(void);
 static void dummy_handler(SIGNAL_ARGS);
 static void CleanupBackend(PMChild *bp, int exitstatus);
 static void HandleChildCrash(int pid, int exitstatus, const char *procname);
+static void HandleFatalError(QuitSignalReason reason, bool consider_sigabrt);
 static void LogChildExit(int lev, const char *procname,
 						 int pid, int exitstatus);
 static void PostmasterStateMachine(void);
@@ -2333,8 +2334,30 @@ process_pm_child_exit(void)
 				}
 				else
 					StartupStatus = STARTUP_CRASHED;
-				HandleChildCrash(pid, exitstatus,
-								 _("startup process"));
+
+				/*
+				 * If the startup process crashed while we were still
+				 * reinitializing after a previous crash -- FatalError is
+				 * still set, because this startup process died before WAL
+				 * redo started -- then HandleChildCrash() would return
+				 * without doing anything, and nothing else would ever move
+				 * the state machine off PM_STARTUP: we would keep
+				 * relaunching the remaining background processes forever,
+				 * without logging anything.  Instead, give up on startup:
+				 * signal the remaining children and head for
+				 * PM_NO_CHILDREN, where STARTUP_CRASHED makes us exit.
+				 */
+				if (StartupStatus == STARTUP_CRASHED &&
+					FatalError && Shutdown != ImmediateShutdown)
+				{
+					LogChildExit(LOG, _("startup process"), pid, exitstatus);
+					ereport(LOG,
+							(errmsg("aborting startup due to startup process failure")));
+					HandleFatalError(PMQUIT_FOR_CRASH, true);
+				}
+				else
+					HandleChildCrash(pid, exitstatus,
+									 _("startup process"));
 				continue;
 			}
 
@@ -2725,15 +2748,15 @@ CleanupBackend(PMChild *bp,
  * happened. Commonly the caller will have logged the reason for entering
  * FatalError state.
  *
- * This should only be called when not already in FatalError or
- * ImmediateShutdown state.
+ * This should only be called when not already in ImmediateShutdown state.
+ * Calling it again while FatalError is already set is fine (and re-signals
+ * any children launched since the previous call).
  */
 static void
 HandleFatalError(QuitSignalReason reason, bool consider_sigabrt)
 {
 	int			sigtosend;
 
-	Assert(!FatalError);
 	Assert(Shutdown != ImmediateShutdown);
 
 	SetQuitSignalReason(reason);
-- 
2.34.1

