From 3000ad8e1afcca05479ed2d5903214b96dc17227 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Sat, 7 Mar 2026 00:00:38 +0100
Subject: [PATCH v5 4/5] fixup! Don't use deprecated and insecure PQcancel psql
 and other tools anymore

Keep track of generation that should be cancelled.
---
 src/fe_utils/cancel.c | 46 ++++++++++++++++++++++++++++++++++---------
 1 file changed, 37 insertions(+), 9 deletions(-)

diff --git a/src/fe_utils/cancel.c b/src/fe_utils/cancel.c
index 5d645c554ab..caaf3e7c675 100644
--- a/src/fe_utils/cancel.c
+++ b/src/fe_utils/cancel.c
@@ -46,6 +46,7 @@
 #include "postgres_fe.h"
 
 #include <signal.h>
+#include <stdatomic.h>
 #include <unistd.h>
 
 #ifndef WIN32
@@ -85,9 +86,22 @@ static PGcancelConn *cancelConn = NULL;
 
 /*
  * Generation counter for cancelConn. Incremented each time cancelConn is
- * changed. Used to detect if cancelConn was replaced while we were using it.
+ * changed (under cancelConnLock). Read by the signal handler (which can run
+ * on any thread) and by the cancel thread, so it needs to be atomic.
  */
-static uint64 cancelConnGeneration = 0;
+static _Atomic sig_atomic_t cancelConnGeneration = 0;
+
+/*
+ * The generation that was current when SIGINT was received.  The cancel
+ * thread compares this against cancelConnGeneration before sending a cancel
+ * request, so that a stale signal (from a query that already finished) does
+ * not cancel a subsequent query.
+ *
+ * Written by the signal handler (or Windows console handler) and read by
+ * the cancel thread, so it must be an atomic type for cross-thread safety.
+ * sig_atomic_t is guaranteed lock-free and thus safe in signal handlers.
+ */
+static _Atomic sig_atomic_t cancelConnGenerationDuringSignal = 0;
 
 /*
  * Mutex protecting cancelConn and cancelConnGeneration.
@@ -151,22 +165,29 @@ static void
 SendCancelRequest(void)
 {
 	PGcancelConn *cc;
-	uint64		generation;
+	sig_atomic_t generation;
 	bool		putConnectionBack = false;
 
 	/*
 	 * Borrow the cancel connection from the global, setting it to NULL so
 	 * that SetCancelConn/ResetCancelConn won't free it while we're using it.
+	 *
+	 * Also check that the generation matches what the signal handler
+	 * captured. If it doesn't, the main thread already moved on to a
+	 * different query (or no query at all), so sending a cancel would be
+	 * wrong.
 	 */
 	pthread_mutex_lock(&cancelConnLock);
 	cc = cancelConn;
-	generation = cancelConnGeneration;
+	generation = atomic_load(&cancelConnGeneration);
+	if (cc == NULL || generation != atomic_load(&cancelConnGenerationDuringSignal))
+	{
+		pthread_mutex_unlock(&cancelConnLock);
+		return;
+	}
 	cancelConn = NULL;
 	pthread_mutex_unlock(&cancelConnLock);
 
-	if (cc == NULL)
-		return;
-
 	write_stderr(cancel_sent_msg);
 
 	if (!PQcancelBlocking(cc))
@@ -185,7 +206,7 @@ SendCancelRequest(void)
 	 * using it.
 	 */
 	pthread_mutex_lock(&cancelConnLock);
-	if (cancelConnGeneration == generation)
+	if (atomic_load(&cancelConnGeneration) == generation)
 	{
 		/* Generation unchanged, put it back for reuse */
 		cancelConn = cc;
@@ -210,7 +231,7 @@ SetCancelConnInternal(PGcancelConn *newCancelConn)
 	pthread_mutex_lock(&cancelConnLock);
 	oldCancelConn = cancelConn;
 	cancelConn = newCancelConn;
-	cancelConnGeneration++;
+	atomic_fetch_add(&cancelConnGeneration, 1);
 	pthread_mutex_unlock(&cancelConnLock);
 
 	if (oldCancelConn != NULL)
@@ -256,6 +277,7 @@ consoleHandler(DWORD dwCtrlType)
 		dwCtrlType == CTRL_BREAK_EVENT)
 	{
 		CancelRequested = true;
+		atomic_store(&cancelConnGenerationDuringSignal, atomic_load(&cancelConnGeneration));
 
 		if (cancel_callback != NULL)
 			cancel_callback();
@@ -311,6 +333,12 @@ handle_sigint(SIGNAL_ARGS)
 
 	CancelRequested = true;
 
+	/*
+	 * Capture the current generation so the cancel thread can verify it's
+	 * still sending the cancel for the right query.
+	 */
+	atomic_store(&cancelConnGenerationDuringSignal, atomic_load(&cancelConnGeneration));
+
 	if (cancel_callback != NULL)
 		cancel_callback();
 
-- 
2.53.0

