From c074e30b4c4269158ab1bfd3c5147462734cc8df Mon Sep 17 00:00:00 2001
From: Nikhil Sontakke <nikhil@planetscale.com>
Date: Fri, 7 Aug 2026 14:15:51 +0530
Subject: [PATCH 2/2] Fix hot standby accepting connections too early after a
 crash reset

Commit b53b88109f9 made the postmaster maintain reachedConsistency in
addition to the startup process.  Since the startup process is forked
from the postmaster, it begins life holding whatever value the
postmaster last set.

On a crash reset the postmaster re-forks the startup process while its
own copy still says true: it clears that copy only on receipt of
PMSIGNAL_RECOVERY_STARTED, which the replacement process cannot send
before it exists.  The replacement therefore starts out believing the
database is already consistent.

CheckRecoveryConsistency() then skips the minRecoveryPoint comparison
altogether, so hot standby is announced at redo start while replay may
be arbitrarily far behind minRecoveryPoint.  Read-only connections are
accepted and answer from heap pages that were flushed ahead of the
replay position, returning wrong results with no error raised.  The
same branch also runs XLogCheckInvalidPages() and
CheckTablespaceDirectory(), which are skipped as well, and
log_invalid_page() treats page references that are normal before
consistency as a PANIC.

Fix by clearing reachedConsistency in InitWalRecovery(), so that a
startup process never depends on the value it inherited.  The
postmaster's own copy is deliberately left alone: forked backends read
it to choose the "not yet accepting connections" errdetail, and it
converges once the new startup process sends PMSIGNAL_RECOVERY_STARTED
and, on reaching minRecoveryPoint, PMSIGNAL_RECOVERY_CONSISTENT.

Successive crash resets alternate.  A startup process that skips the
branch never sends PMSIGNAL_RECOVERY_CONSISTENT, so the postmaster's
copy stays false and the next reset forks a process holding the correct
value; that pass reaches consistency properly, which sets the
postmaster's copy back to true and re-arms the problem for the reset
after it.  Roughly every other crash reset is therefore affected, not
just the first one.  EXEC_BACKEND builds are unaffected, as
reachedConsistency is not carried in BackendParameters.

The TAP test added by the preceding patch covers this.

Reported-by: Eric Ridge <eebbrr@planetscale.com>
Backpatch-through: 18
---
 src/backend/access/transam/xlogrecovery.c | 26 +++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 5f3b065b894..4163784f253 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -471,6 +471,32 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 
 	dbstate_at_startup = ControlFile->state;
 
+	/*
+	 * A startup process always begins with a database that is not yet known
+	 * to be consistent, so make sure the flag says so.  The postmaster
+	 * maintains a copy of this variable as well (see process_pm_pmsignal()),
+	 * and a startup process forked once the postmaster's copy has turned true
+	 * inherits that value across the fork, which would make
+	 * CheckRecoveryConsistency() skip the minRecoveryPoint comparison
+	 * altogether.  That is what happens on a crash reset: the postmaster
+	 * re-forks the startup process while still holding true, because it only
+	 * clears its own copy on receipt of PMSIGNAL_RECOVERY_STARTED, which the
+	 * replacement process cannot send until it already exists.  Such a
+	 * process would announce hot standby at redo start with replay
+	 * arbitrarily far behind minRecoveryPoint.
+	 *
+	 * Clearing it here, rather than in the postmaster before it forks, keeps
+	 * the invariant with the process that owns it: a startup process is then
+	 * correct on its own terms whatever the postmaster holds, and no present
+	 * or future fork path has to remember to clear it first.  This point also
+	 * precedes every reader, since they all run once redo is under way.  The
+	 * postmaster's own copy is deliberately left alone: that is what forked
+	 * backends inherit for the "not yet accepting connections" DETAIL, and it
+	 * converges by itself once this process sends PMSIGNAL_RECOVERY_STARTED
+	 * and, on reaching minRecoveryPoint, PMSIGNAL_RECOVERY_CONSISTENT.
+	 */
+	reachedConsistency = false;
+
 	/*
 	 * Initialize on the assumption we want to recover to the latest timeline
 	 * that's active according to pg_control.
-- 
2.50.1 (Apple Git-155)

