From 5b4735b8b3c866f5739f162f38992c5a04b6abde Mon Sep 17 00:00:00 2001 From: Jeevan Chalke Date: Wed, 23 Sep 2026 14:59:18 +0530 Subject: [PATCH v2 2/2] Add regression test for shared-partial-path use-after-free Add two tests to select_parallel.sql reproducing the crash fixed by the previous commit, from independent reports: - 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. The crash happens during planning, so the plain SELECT alone already exercises it -- no EXPLAIN needed, keeping the expected output small and independent of plan shape, which could easily vary across backpatched versions. - A single-table UNION arm with a set-returning function forcing current_rel and final_rel apart even without partitioning, where two competing partial index paths on tenk1 tie in cost once the SRF's cost swamps the difference between them. 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 the first test needs. --- src/test/regress/expected/select_parallel.out | 88 +++++++++++++++++++ src/test/regress/sql/select_parallel.sql | 79 +++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out index e1344215644..979036de51a 100644 --- a/src/test/regress/expected/select_parallel.out +++ b/src/test/regress/expected/select_parallel.out @@ -1298,6 +1298,94 @@ 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; +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) + +-- Same underlying issue, triggered a different way: a set-returning +-- function in the target list forces current_rel and final_rel apart even +-- for this single-relation query, and two competing partial index paths +-- tie in cost once the SRF's cost swamps the difference between them. +SET LOCAL min_parallel_index_scan_size = 0; +SET LOCAL max_parallel_workers_per_gather = 1; +EXPLAIN (COSTS OFF) +SELECT unique1, generate_series(1, 5000000) FROM tenk1 WHERE unique2 < 100 +UNION +SELECT 1, 2; + QUERY PLAN +-------------------------------------------------------------------------------- + Unique + -> Sort + Sort Key: tenk1.unique1, (generate_series(1, 5000000)) + -> Append + -> Gather + Workers Planned: 1 + -> ProjectSet + -> Parallel Index Scan using tenk1_unique2 on tenk1 + Index Cond: (unique2 < 100) + -> Result +(10 rows) + +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..8a6b5302896 100644 --- a/src/test/regress/sql/select_parallel.sql +++ b/src/test/regress/sql/select_parallel.sql @@ -495,6 +495,85 @@ 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; + +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; + +-- Same underlying issue, triggered a different way: a set-returning +-- function in the target list forces current_rel and final_rel apart even +-- for this single-relation query, and two competing partial index paths +-- tie in cost once the SRF's cost swamps the difference between them. +SET LOCAL min_parallel_index_scan_size = 0; +SET LOCAL max_parallel_workers_per_gather = 1; + +EXPLAIN (COSTS OFF) +SELECT unique1, generate_series(1, 5000000) FROM tenk1 WHERE unique2 < 100 +UNION +SELECT 1, 2; + +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