From 9b4f7d5f4f3a8ebadcabda6f1993790441a75b80 Mon Sep 17 00:00:00 2001
From: Emond Papegaaij <emond.papegaaij@topicus.nl>
Date: Thu, 11 Jun 2026 09:37:44 +0200
Subject: [PATCH] Fix use-after-free of query context after a backend node
 shutdown

A POOL_QUERY_CONTEXT is referenced from the session scoped sent message
list (session_context->message_list) and pending message list, which
live in session_context->memory_context and survive for the whole
client session.  However pool_init_query_context() allocated the query
context in the global QueryContext memory context.

do_child() resets QueryContext (MemoryContextResetAndDeleteChildren)
at the top of every iteration of its query processing loop.  In normal
operation pool_process_query() runs the whole session in a single
iteration, so QueryContext is effectively session lived and this is
harmless.  But when pool_process_query() returns POOL_CONTINUE in the
middle of a session, the loop iterates and QueryContext is reset while
the sent/pending message lists still reference query contexts that were
allocated in it.  The freed query contexts are later dereferenced (for
example from pool_clear_sent_message_list() in reset_connection(), or
from pool_remove_sent_message() while binding a reused statement name),
causing a use-after-free and a SIGSEGV.

This is reachable when a backend node is shut down by an administrative
command (e.g. "fast" shutdown of PostgreSQL) while a client session that
has named prepared statements is open.  read_packets_and_process()
detects the admin shutdown, sets was_error and returns POOL_CONTINUE
(cont = false), so pool_process_query() returns to do_child() mid
session and the next loop iteration frees the still referenced query
contexts.

Fix this by allocating the query context under the session memory
context rather than the per-iteration QueryContext, so its lifetime
matches the session scoped lists that reference it.  When there is no
session context (internal queries issued before a session exists) fall
back to QueryContext, preserving the previous behaviour.  Query contexts
are still freed explicitly by pool_query_context_destroy(), and at the
latest when the session memory context is destroyed, so this does not
change memory reclamation for the normal single-iteration case (in which
QueryContext was likewise only freed at session end).
---
 src/context/pool_query_context.c | 34 +++++++++++++++++++++++++-------
 1 file changed, 27 insertions(+), 7 deletions(-)

diff --git a/src/context/pool_query_context.c b/src/context/pool_query_context.c
index a056ac596..7eef39367 100644
--- a/src/context/pool_query_context.c
+++ b/src/context/pool_query_context.c
@@ -78,15 +78,35 @@ static char *get_associated_object_from_dml_adaptive_relations
 POOL_QUERY_CONTEXT *
 pool_init_query_context(void)
 {
-	MemoryContext memory_context = AllocSetContextCreate(QueryContext,
-														 "QueryContextMemoryContext",
-														 ALLOCSET_SMALL_MINSIZE,
-														 ALLOCSET_SMALL_INITSIZE,
-														 ALLOCSET_SMALL_MAXSIZE);
-
-	MemoryContext oldcontext = MemoryContextSwitchTo(memory_context);
+	POOL_SESSION_CONTEXT *session_context;
+	MemoryContext parent;
+	MemoryContext memory_context;
+	MemoryContext oldcontext;
 	POOL_QUERY_CONTEXT *qc;
 
+	/*
+	 * Parent the query context under the session memory context rather than
+	 * the per-iteration QueryContext.  A query context is referenced from the
+	 * session scoped sent message list and pending message list.  These lists
+	 * outlive the QueryContext, which do_child() resets between the iterations
+	 * of its query processing loop (for instance when pool_process_query()
+	 * returns after a backend node was shut down).  If the query context lived
+	 * in QueryContext, such a reset would free it while the message lists
+	 * still reference it, resulting in a use-after-free.  When there is no
+	 * session context (e.g. internal queries issued before a session exists),
+	 * fall back to QueryContext.
+	 */
+	session_context = pool_get_session_context(true);
+	parent = session_context ? session_context->memory_context : QueryContext;
+
+	memory_context = AllocSetContextCreate(parent,
+										   "QueryContextMemoryContext",
+										   ALLOCSET_SMALL_MINSIZE,
+										   ALLOCSET_SMALL_INITSIZE,
+										   ALLOCSET_SMALL_MAXSIZE);
+
+	oldcontext = MemoryContextSwitchTo(memory_context);
+
 	qc = palloc0(sizeof(*qc));
 	qc->memory_context = memory_context;
 	MemoryContextSwitchTo(oldcontext);
-- 
2.53.0

