v2-0001-Fix-EPQ-crash-from-missing-partition-directory-in.patch

application/octet-stream

Filename: v2-0001-Fix-EPQ-crash-from-missing-partition-directory-in.patch
Type: application/octet-stream
Part: 0
Message: Re: BUG #19078: Segfaults in tts_minimal_store_tuple() following pg_upgrade

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v2-0001
Subject: Fix EPQ crash from missing partition directory in EState
File+
src/backend/executor/execMain.c 10 0
src/test/isolation/expected/eval-plan-qual.out 7 0
src/test/isolation/specs/eval-plan-qual.spec 2 0
From 75eb867c3f91c085dbd9be856db90e4f72ccd86f Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Wed, 15 Oct 2025 23:22:43 -0400
Subject: [PATCH v2] Fix EPQ crash from missing partition directory in EState
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

EvalPlanQualStart() failed to propagate es_partition_directory into
the child EState used for EPQ rechecks. When execution time partition
pruning ran during the EPQ scan, executor code dereferenced a NULL
partition directory and crashed.

Before commit d47cbf474, CreatePartitionPruneState(), called from
ExecInitNode(), created es_partition_directory on demand. After that
refactor it initializes only the init time pruning context, while
ExecInitNode() initializes the exec time context. Since
EvalPlanQualStart() runs only ExecInitNode() and not
CreatePartitionPruneState(), the EPQ estate can be left with a NULL
es_partition_directory unless copied from the parent.

Fix by borrowing the parent estate’s es_partition_directory in
EvalPlanQualStart(), and by clearing that field in EvalPlanQualEnd()
so the parent remains responsible for freeing the directory.

Add an isolation test permutation that triggers EPQ with execution-
time partition pruning, the case that reproduces this crash.

Bug: #19078
Reported-by: Yuri Zamyatin <yuri@yrz.am>
Diagnosed-by: David Rowley <dgrowleyml@gmail.com>
Author: David Rowley <dgrowleyml@gmail.com>
Co-authored-by: Amit Langote <amitlangote09@gmail.com>
Discussion: https://postgr.es/m/19078-dfd62f840a2c0766@postgresql.org
---
 src/backend/executor/execMain.c                | 10 ++++++++++
 src/test/isolation/expected/eval-plan-qual.out |  7 +++++++
 src/test/isolation/specs/eval-plan-qual.spec   |  2 ++
 3 files changed, 19 insertions(+)

diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 831c55ce787..713e926329c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -3093,6 +3093,9 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree)
 	rcestate->es_part_prune_states = parentestate->es_part_prune_states;
 	rcestate->es_part_prune_results = parentestate->es_part_prune_results;
 
+	/* We'll also borrow the es_partition_directory from the parent state */
+	rcestate->es_partition_directory = parentestate->es_partition_directory;
+
 	/*
 	 * Initialize private state information for each SubPlan.  We must do this
 	 * before running ExecInitNode on the main query tree, since
@@ -3210,6 +3213,13 @@ EvalPlanQualEnd(EPQState *epqstate)
 
 	MemoryContextSwitchTo(oldcontext);
 
+	/*
+	 * NULLify the partition directory before freeing the executor state.
+	 * Since EvalPlanQualStart() just borrowed the parent EState's directory,
+	 * we'd better leave it up to the parent to delete it.
+	 */
+	estate->es_partition_directory = NULL;
+
 	FreeExecutorState(estate);
 
 	/* Mark EPQState idle */
diff --git a/src/test/isolation/expected/eval-plan-qual.out b/src/test/isolation/expected/eval-plan-qual.out
index 60eca44b4e3..05fffe0d570 100644
--- a/src/test/isolation/expected/eval-plan-qual.out
+++ b/src/test/isolation/expected/eval-plan-qual.out
@@ -1466,3 +1466,10 @@ step s2pp3: EXECUTE epd(1); <waiting ...>
 step c1: COMMIT;
 step s2pp3: <... completed>
 step c2: COMMIT;
+
+starting permutation: s1pp1 s2pp4 c1 c2
+step s1pp1: UPDATE another_parttbl SET b = b + 1 WHERE a = 1;
+step s2pp4: DELETE FROM another_parttbl WHERE a = (SELECT 1); <waiting ...>
+step c1: COMMIT;
+step s2pp4: <... completed>
+step c2: COMMIT;
diff --git a/src/test/isolation/specs/eval-plan-qual.spec b/src/test/isolation/specs/eval-plan-qual.spec
index 64afffb1d83..80e1e6bb307 100644
--- a/src/test/isolation/specs/eval-plan-qual.spec
+++ b/src/test/isolation/specs/eval-plan-qual.spec
@@ -316,6 +316,7 @@ step r2	{ ROLLBACK; }
 step s2pp1 { SET plan_cache_mode TO force_generic_plan; }
 step s2pp2 { PREPARE epd AS DELETE FROM another_parttbl WHERE a = $1; }
 step s2pp3 { EXECUTE epd(1); }
+step s2pp4 { DELETE FROM another_parttbl WHERE a = (SELECT 1); }
 
 session s3
 setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
@@ -423,3 +424,4 @@ permutation sys1 sysmerge2 c1 c2
 
 # Exercise run-time partition pruning code in an EPQ recheck
 permutation s1pp1 s2pp1 s2pp2 s2pp3 c1 c2
+permutation s1pp1 s2pp4 c1 c2
-- 
2.47.3