From 77c06b65c2b7b5cc8ff98636f7ba41c2fd2c7700 Mon Sep 17 00:00:00 2001
From: Jingtang Zhang <mrdrivingduck@gmail.com>
Date: Mon, 2 Nov 2026 17:33:45 +0800
Subject: [PATCH v1] Clear auxiliary resource owner hash before reuse

Bulk release sorts and compacts the resource hash table but only resets
its item count when releasing resources. Entries in the sorted prefix
remain populated, and compaction can leave extra copies outside that
prefix.

An auxiliary process reuses its owner after releasing its resources.
Clear the whole hash table before allowing reuse so that subsequent
resource acquisition cannot treat stale entries as live resources or
release them twice.

Backpatch-through: 17
---
 src/backend/utils/resowner/resowner.c         |  9 ++
 .../test_resowner/expected/test_resowner.out  | 25 ++++++
 .../test_resowner/sql/test_resowner.sql       |  6 ++
 .../test_resowner/test_resowner--1.0.sql      |  4 +
 .../test_resowner/test_resowner_basic.c       | 87 +++++++++++++++++++
 5 files changed, 131 insertions(+)

diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c
index ac413d54837..149b5273667 100644
--- a/src/backend/utils/resowner/resowner.c
+++ b/src/backend/utils/resowner/resowner.c
@@ -1038,6 +1038,15 @@ ReleaseAuxProcessResources(bool isCommit)
 	ResourceOwnerRelease(AuxProcessResourceOwner,
 						 RESOURCE_RELEASE_AFTER_LOCKS,
 						 isCommit, true);
+
+	/*
+	 * Sorting can leave copies of entries beyond the sorted prefix, and bulk
+	 * release only resets the counts.  Clear the entire table before reuse.
+	 */
+	if (AuxProcessResourceOwner->hash != NULL)
+		memset(AuxProcessResourceOwner->hash, 0,
+			   AuxProcessResourceOwner->capacity * sizeof(ResourceElem));
+
 	/* allow it to be reused */
 	AuxProcessResourceOwner->releasing = false;
 	AuxProcessResourceOwner->sorted = false;
diff --git a/src/test/modules/test_resowner/expected/test_resowner.out b/src/test/modules/test_resowner/expected/test_resowner.out
index 527b678fc12..0c8cf57f3f4 100644
--- a/src/test/modules/test_resowner/expected/test_resowner.out
+++ b/src/test/modules/test_resowner/expected/test_resowner.out
@@ -195,3 +195,28 @@ ERROR:  ResourceOwnerEnlarge called after release started
 SELECT test_resowner_forget_between_phases();
 ERROR:  ResourceOwnerForget called for test resource after release started
 reset client_min_messages;
+-- Reuse an auxiliary resource owner with and without a hash table.
+SELECT test_resowner_aux(16);
+ test_resowner_aux 
+-------------------
+ 
+(1 row)
+
+SELECT test_resowner_aux(64);
+ test_resowner_aux 
+-------------------
+ 
+(1 row)
+
+SELECT test_resowner_aux(65);
+ test_resowner_aux 
+-------------------
+ 
+(1 row)
+
+SELECT test_resowner_aux(1024);
+ test_resowner_aux 
+-------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_resowner/sql/test_resowner.sql b/src/test/modules/test_resowner/sql/test_resowner.sql
index 23284b7c8b9..eb1afc0cc82 100644
--- a/src/test/modules/test_resowner/sql/test_resowner.sql
+++ b/src/test/modules/test_resowner/sql/test_resowner.sql
@@ -23,3 +23,9 @@ set client_min_messages='warning'; -- order between ERROR and NOTICE varies
 SELECT test_resowner_remember_between_phases();
 SELECT test_resowner_forget_between_phases();
 reset client_min_messages;
+
+-- Reuse an auxiliary resource owner with and without a hash table.
+SELECT test_resowner_aux(16);
+SELECT test_resowner_aux(64);
+SELECT test_resowner_aux(65);
+SELECT test_resowner_aux(1024);
diff --git a/src/test/modules/test_resowner/test_resowner--1.0.sql b/src/test/modules/test_resowner/test_resowner--1.0.sql
index 26fed7a010c..48dc6ed5d2c 100644
--- a/src/test/modules/test_resowner/test_resowner--1.0.sql
+++ b/src/test/modules/test_resowner/test_resowner--1.0.sql
@@ -19,6 +19,10 @@ CREATE FUNCTION test_resowner_forget_between_phases()
 	RETURNS pg_catalog.void
 	AS 'MODULE_PATHNAME' LANGUAGE C;
 
+CREATE FUNCTION test_resowner_aux(nresources pg_catalog.int4)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
 CREATE FUNCTION test_resowner_many(
    nkinds       pg_catalog.int4,
    nremember_bl pg_catalog.int4,
diff --git a/src/test/modules/test_resowner/test_resowner_basic.c b/src/test/modules/test_resowner/test_resowner_basic.c
index c0891f7e539..fca9fb246b7 100644
--- a/src/test/modules/test_resowner/test_resowner_basic.c
+++ b/src/test/modules/test_resowner/test_resowner_basic.c
@@ -19,6 +19,7 @@ PG_MODULE_MAGIC;
 
 static void ReleaseString(Datum res);
 static char *PrintString(Datum res);
+static void CountRelease(Datum res);
 
 /*
  * A resource that tracks strings and prints the string when it's released.
@@ -207,3 +208,89 @@ test_resowner_forget_between_phases(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+static void
+CountRelease(Datum res)
+{
+	int		   *nreleased = (int *) DatumGetPointer(res);
+
+	(*nreleased)++;
+}
+
+/*
+ * Check that an auxiliary resource owner can be reused after bulk release.
+ */
+PG_FUNCTION_INFO_V1(test_resowner_aux);
+Datum
+test_resowner_aux(PG_FUNCTION_ARGS)
+{
+	static const ResourceOwnerDesc before_desc = {
+		.name = "test resource before locks",
+		.release_phase = RESOURCE_RELEASE_BEFORE_LOCKS,
+		.release_priority = RELEASE_PRIO_FIRST,
+		.ReleaseResource = CountRelease
+	};
+	static const ResourceOwnerDesc after_desc = {
+		.name = "test resource after locks",
+		.release_phase = RESOURCE_RELEASE_AFTER_LOCKS,
+		.release_priority = RELEASE_PRIO_FIRST,
+		.ReleaseResource = CountRelease
+	};
+	int32		nresources = PG_GETARG_INT32(0);
+	int		   *nreleased[2];
+	ResourceOwner resowner;
+	ResourceOwner save_current = CurrentResourceOwner;
+	ResourceOwner save_aux = AuxProcessResourceOwner;
+
+	if (nresources <= 0)
+		elog(ERROR, "nresources must be greater than zero");
+
+	nreleased[0] = palloc0_array(int, nresources);
+	nreleased[1] = palloc0_array(int, nresources);
+	resowner = ResourceOwnerCreate(CurrentResourceOwner, "test auxiliary owner");
+
+	/*
+	 * Use an isolated owner without registering an auxiliary-process exit
+	 * hook.
+	 */
+	PG_TRY();
+	{
+		CurrentResourceOwner = AuxProcessResourceOwner = resowner;
+		for (int round = 0; round < 2; round++)
+		{
+			for (int i = 0; i < nresources; i++)
+			{
+				const ResourceOwnerDesc *kind = i % 2 == 0 ? &before_desc : &after_desc;
+
+				ResourceOwnerEnlarge(resowner);
+				ResourceOwnerRemember(resowner,
+									  PointerGetDatum(&nreleased[round][i]), kind);
+			}
+			ReleaseAuxProcessResources(false);
+		}
+	}
+	PG_FINALLY();
+	{
+		CurrentResourceOwner = save_current;
+		AuxProcessResourceOwner = save_aux;
+	}
+	PG_END_TRY();
+
+	ResourceOwnerDelete(resowner);
+
+	/*
+	 * Keep both rounds' counters alive to detect releases of stale entries.
+	 */
+	for (int round = 0; round < 2; round++)
+	{
+		for (int i = 0; i < nresources; i++)
+		{
+			if (nreleased[round][i] != 1)
+				elog(ERROR, "resource %d in round %d was released %d times",
+					 i, round, nreleased[round][i]);
+		}
+		pfree(nreleased[round]);
+	}
+
+	PG_RETURN_VOID();
+}

base-commit: cea96cdfecbf40aa3f12c1c2572c191f27c318d0
-- 
2.43.7

