From d3dce4bb21323a3226181b51a2eeac83e6d11d64 Mon Sep 17 00:00:00 2001
From: Jeevan Chalke <jeevan.chalke@enterprisedb.com>
Date: Wed, 9 Sep 2026 13:20:59 +0530
Subject: [PATCH v1 2/2] Add regression test for shared-partial-path
 use-after-free

Add a test to select_parallel.sql reproducing the crash fixed by the
previous commit: a UNION branch joining two 7+ partition relations
with a set-returning function in the target list, under parallel
query. At least 7 partitions per relation are needed to get enough
competing partial-path candidates for the crash's dominance check to
trigger.

Pins max_parallel_workers_per_gather locally, since an earlier
non-local SET in this file otherwise changes the ambient worker count
enough to avoid the conflict this test needs.
---
 src/test/regress/expected/select_parallel.out | 124 ++++++++++++++++++
 src/test/regress/sql/select_parallel.sql      |  74 +++++++++++
 2 files changed, 198 insertions(+)

diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 0185ef661b1..371d9e90b18 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -1298,6 +1298,130 @@ SELECT unnest(ARRAY[]::integer[]) + 1 AS pathkey
                                  ->  Parallel Index Only Scan using tenk1_hundred on public.tenk1 t2
 (15 rows)
 
+-- Regression test for a use-after-free of a shared partial path.
+--
+-- Planning a UNION branch recurses into its own subquery_planner() call.
+-- If that branch's own scan/join rel builds a Gather/Gather Merge path
+-- over one of its partial paths, and grouping_planner() then promotes
+-- that same (still partial_pathlist-resident) partial path, unmodified,
+-- to the outer query level's final rel so an outer Gather could use it,
+-- a dominance comparison against another promoted candidate could decide
+-- to pfree() it -- even though the branch's own Gather/Gather Merge path
+-- still points to it.  This crashed with "unrecognized node type: N" once
+-- the freed memory got reused.  Needs at least 7 partitions per relation
+-- to get enough competing partial-path candidates for the promotion's own
+-- dominance check to discard one that a Gather still depends on.
+SAVEPOINT settings;
+SET LOCAL parallel_setup_cost = 0;
+SET LOCAL parallel_tuple_cost = 0;
+SET LOCAL min_parallel_table_scan_size = 0;
+SET LOCAL max_parallel_workers_per_gather = 2;
+CREATE TABLE psrf_upload (
+    id int,
+    crt_time timestamp,
+    jdata jsonb,
+    tag text
+) PARTITION BY RANGE (crt_time);
+CREATE TABLE psrf_upload_dtl (
+    id int,
+    upload_id int,
+    crt_time timestamp,
+    jdata jsonb
+) PARTITION BY RANGE (crt_time);
+CREATE TABLE psrf_upload_p0 PARTITION OF psrf_upload FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
+CREATE TABLE psrf_upload_p1 PARTITION OF psrf_upload FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
+CREATE TABLE psrf_upload_p2 PARTITION OF psrf_upload FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
+CREATE TABLE psrf_upload_p3 PARTITION OF psrf_upload FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');
+CREATE TABLE psrf_upload_p4 PARTITION OF psrf_upload FOR VALUES FROM ('2024-05-01') TO ('2024-06-01');
+CREATE TABLE psrf_upload_p5 PARTITION OF psrf_upload FOR VALUES FROM ('2024-06-01') TO ('2024-07-01');
+CREATE TABLE psrf_upload_p6 PARTITION OF psrf_upload FOR VALUES FROM ('2024-07-01') TO ('2024-08-01');
+CREATE TABLE psrf_upload_dtl_p0 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
+CREATE TABLE psrf_upload_dtl_p1 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
+CREATE TABLE psrf_upload_dtl_p2 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
+CREATE TABLE psrf_upload_dtl_p3 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');
+CREATE TABLE psrf_upload_dtl_p4 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-05-01') TO ('2024-06-01');
+CREATE TABLE psrf_upload_dtl_p5 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-06-01') TO ('2024-07-01');
+CREATE TABLE psrf_upload_dtl_p6 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-07-01') TO ('2024-08-01');
+INSERT INTO psrf_upload VALUES
+  (1, '2024-05-01 00:00:00', jsonb_build_array(jsonb_build_object('v', repeat('x', 1000))), 'tag1');
+INSERT INTO psrf_upload_dtl
+  SELECT id, id, crt_time, jsonb_build_array(jsonb_build_object('v', repeat('x', 1000)))
+  FROM psrf_upload;
+ANALYZE psrf_upload;
+ANALYZE psrf_upload_dtl;
+EXPLAIN (COSTS OFF)
+SELECT u.id, u.tag, jsonb_array_elements(u.jdata)->>'v' AS val
+FROM psrf_upload u
+UNION
+SELECT u.id, u.tag, jsonb_array_elements(d.jdata)->>'v' AS val
+FROM psrf_upload u LEFT JOIN psrf_upload_dtl d ON d.upload_id = u.id;
+                                         QUERY PLAN                                          
+---------------------------------------------------------------------------------------------
+ HashAggregate
+   Group Key: u.id, u.tag, (((jsonb_array_elements(u.jdata)) ->> 'v'::text))
+   ->  Append
+         ->  Gather
+               Workers Planned: 2
+               ->  Parallel Append
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Seq Scan on psrf_upload_p0 u_1
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Seq Scan on psrf_upload_p1 u_2
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Seq Scan on psrf_upload_p2 u_3
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Seq Scan on psrf_upload_p3 u_4
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Seq Scan on psrf_upload_p5 u_6
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Seq Scan on psrf_upload_p6 u_7
+                     ->  Result
+                           ->  ProjectSet
+                                 ->  Parallel Seq Scan on psrf_upload_p4 u_5
+         ->  Gather
+               Workers Planned: 2
+               ->  Result
+                     ->  ProjectSet
+                           ->  Parallel Hash Left Join
+                                 Hash Cond: (u_8.id = d.upload_id)
+                                 ->  Parallel Append
+                                       ->  Seq Scan on psrf_upload_p0 u_9
+                                       ->  Seq Scan on psrf_upload_p1 u_10
+                                       ->  Seq Scan on psrf_upload_p2 u_11
+                                       ->  Seq Scan on psrf_upload_p3 u_12
+                                       ->  Seq Scan on psrf_upload_p5 u_14
+                                       ->  Seq Scan on psrf_upload_p6 u_15
+                                       ->  Parallel Seq Scan on psrf_upload_p4 u_13
+                                 ->  Parallel Hash
+                                       ->  Parallel Append
+                                             ->  Seq Scan on psrf_upload_dtl_p0 d_1
+                                             ->  Seq Scan on psrf_upload_dtl_p1 d_2
+                                             ->  Seq Scan on psrf_upload_dtl_p2 d_3
+                                             ->  Seq Scan on psrf_upload_dtl_p3 d_4
+                                             ->  Seq Scan on psrf_upload_dtl_p5 d_6
+                                             ->  Seq Scan on psrf_upload_dtl_p6 d_7
+                                             ->  Parallel Seq Scan on psrf_upload_dtl_p4 d_5
+(50 rows)
+
+SELECT id, tag, length(val) FROM (
+  SELECT u.id, u.tag, jsonb_array_elements(u.jdata)->>'v' AS val
+  FROM psrf_upload u
+  UNION
+  SELECT u.id, u.tag, jsonb_array_elements(d.jdata)->>'v' AS val
+  FROM psrf_upload u LEFT JOIN psrf_upload_dtl d ON d.upload_id = u.id
+) s;
+ id | tag  | length 
+----+------+--------
+  1 | tag1 |   1000
+(1 row)
+
+ROLLBACK TO SAVEPOINT settings;
 -- test passing expanded-value representations to workers
 CREATE FUNCTION make_some_array(int,int) returns int[] as
 $$declare x int[];
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 71a75bc86ea..23782d746cc 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -495,6 +495,80 @@ SELECT unnest(ARRAY[]::integer[]) + 1 AS pathkey
   FROM tenk1 t1 JOIN tenk1 t2 ON TRUE
   ORDER BY pathkey;
 
+-- Regression test for a use-after-free of a shared partial path.
+--
+-- Planning a UNION branch recurses into its own subquery_planner() call.
+-- If that branch's own scan/join rel builds a Gather/Gather Merge path
+-- over one of its partial paths, and grouping_planner() then promotes
+-- that same (still partial_pathlist-resident) partial path, unmodified,
+-- to the outer query level's final rel so an outer Gather could use it,
+-- a dominance comparison against another promoted candidate could decide
+-- to pfree() it -- even though the branch's own Gather/Gather Merge path
+-- still points to it.  This crashed with "unrecognized node type: N" once
+-- the freed memory got reused.  Needs at least 7 partitions per relation
+-- to get enough competing partial-path candidates for the promotion's own
+-- dominance check to discard one that a Gather still depends on.
+SAVEPOINT settings;
+SET LOCAL parallel_setup_cost = 0;
+SET LOCAL parallel_tuple_cost = 0;
+SET LOCAL min_parallel_table_scan_size = 0;
+SET LOCAL max_parallel_workers_per_gather = 2;
+
+CREATE TABLE psrf_upload (
+    id int,
+    crt_time timestamp,
+    jdata jsonb,
+    tag text
+) PARTITION BY RANGE (crt_time);
+CREATE TABLE psrf_upload_dtl (
+    id int,
+    upload_id int,
+    crt_time timestamp,
+    jdata jsonb
+) PARTITION BY RANGE (crt_time);
+
+CREATE TABLE psrf_upload_p0 PARTITION OF psrf_upload FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
+CREATE TABLE psrf_upload_p1 PARTITION OF psrf_upload FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
+CREATE TABLE psrf_upload_p2 PARTITION OF psrf_upload FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
+CREATE TABLE psrf_upload_p3 PARTITION OF psrf_upload FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');
+CREATE TABLE psrf_upload_p4 PARTITION OF psrf_upload FOR VALUES FROM ('2024-05-01') TO ('2024-06-01');
+CREATE TABLE psrf_upload_p5 PARTITION OF psrf_upload FOR VALUES FROM ('2024-06-01') TO ('2024-07-01');
+CREATE TABLE psrf_upload_p6 PARTITION OF psrf_upload FOR VALUES FROM ('2024-07-01') TO ('2024-08-01');
+
+CREATE TABLE psrf_upload_dtl_p0 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
+CREATE TABLE psrf_upload_dtl_p1 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
+CREATE TABLE psrf_upload_dtl_p2 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
+CREATE TABLE psrf_upload_dtl_p3 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');
+CREATE TABLE psrf_upload_dtl_p4 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-05-01') TO ('2024-06-01');
+CREATE TABLE psrf_upload_dtl_p5 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-06-01') TO ('2024-07-01');
+CREATE TABLE psrf_upload_dtl_p6 PARTITION OF psrf_upload_dtl FOR VALUES FROM ('2024-07-01') TO ('2024-08-01');
+
+INSERT INTO psrf_upload VALUES
+  (1, '2024-05-01 00:00:00', jsonb_build_array(jsonb_build_object('v', repeat('x', 1000))), 'tag1');
+INSERT INTO psrf_upload_dtl
+  SELECT id, id, crt_time, jsonb_build_array(jsonb_build_object('v', repeat('x', 1000)))
+  FROM psrf_upload;
+
+ANALYZE psrf_upload;
+ANALYZE psrf_upload_dtl;
+
+EXPLAIN (COSTS OFF)
+SELECT u.id, u.tag, jsonb_array_elements(u.jdata)->>'v' AS val
+FROM psrf_upload u
+UNION
+SELECT u.id, u.tag, jsonb_array_elements(d.jdata)->>'v' AS val
+FROM psrf_upload u LEFT JOIN psrf_upload_dtl d ON d.upload_id = u.id;
+
+SELECT id, tag, length(val) FROM (
+  SELECT u.id, u.tag, jsonb_array_elements(u.jdata)->>'v' AS val
+  FROM psrf_upload u
+  UNION
+  SELECT u.id, u.tag, jsonb_array_elements(d.jdata)->>'v' AS val
+  FROM psrf_upload u LEFT JOIN psrf_upload_dtl d ON d.upload_id = u.id
+) s;
+
+ROLLBACK TO SAVEPOINT settings;
+
 -- test passing expanded-value representations to workers
 CREATE FUNCTION make_some_array(int,int) returns int[] as
 $$declare x int[];
-- 
2.43.0

