From d3dce4bb21323a3226181b51a2eeac83e6d11d64 Mon Sep 17 00:00:00 2001 From: Jeevan Chalke 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