On 27/04/2026 10:19, Andrei Lepikhov wrote:
> On 21/04/2026 10:35, David Rowley wrote:
>> IMO, we should write a function like copy_path() or reparent_path(),
>> which creates a copy of the given Path, or the latter also would copy
>> then set the ->parent to the given RelOptInfo. Any time we use a path
>> directly from the pathlist of another RelOptInfo, we should reparent
>> or copy it. We could add an Assert in add_path() to check the new path
>> has the correct parent to help us find the places where we forget to
>> do this.
>
> I've attached the patch so we can keep the discussion going.
While playing with random path choices [1], I found additional cases where a
path is assigned to two different RelOptInfos. See the attachment for a modified
patch.
[1] https://github.com/danolivo/pg-chaos-test
--
regards, Andrei Lepikhov,
pgEdge
From 01c335316fa1ad0905d5e9f2f182eff824e24ff2 Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <[email protected]>
Date: Mon, 27 Apr 2026 09:54:11 +0200
Subject: [PATCH v1] Fix dangling Path pointers when sharing paths across
RelOptInfos
In three places the planner reused the same Path pointer across two RelOptInfo
pathlists without making a copy:
- create_ordered_paths() set sorted_path = input_path when the path was already
sorted, placing the same node in both input_rel->pathlist and
ordered_rel->pathlist.
- grouping_planner() passed paths from current_rel directly into add_path and
add_partial_path.
Since add_path() may pfree paths it displaces, and apply_projection_to_path()
may mutate a path in-place, a path shared between two rels can be freed or
corrupted while still referenced by the other.
Introduce copy_path(), a type-aware shallow copy (palloc + memcpy at the
concrete struct size), and use it at all three sites so each rel owns an
independent top-level node. For T_CustomPath, dispatch to a new optional
CopyCustomPath callback in CustomPathMethods; fall back to sizeof(CustomPath)
memcpy when the callback is not provided. Extensions embedding private
fields beyond sizeof(CustomPath) should implement the callback.
Discussion: https://postgr.es/m/adab9758-f346-4263-93af-3e37b7b315b7%40gmail.com
---
src/backend/optimizer/plan/planner.c | 6 +-
src/backend/optimizer/util/pathnode.c | 158 +++++++++++++++++++++++++-
src/include/nodes/extensible.h | 7 ++
src/include/optimizer/pathnode.h | 1 +
4 files changed, 168 insertions(+), 4 deletions(-)
diff --git a/src/backend/optimizer/plan/planner.c
b/src/backend/optimizer/plan/planner.c
index 4ec76ce31a9..e920cbbaa8f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -2226,7 +2226,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
}
/* And shove it into final_rel */
- add_path(final_rel, path);
+ add_path(final_rel, copy_path(path));
}
/*
@@ -2241,7 +2241,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
{
Path *partial_path = (Path *) lfirst(lc);
- add_partial_path(final_rel, partial_path);
+ add_partial_path(final_rel, copy_path(partial_path));
}
}
@@ -5440,7 +5440,7 @@ create_ordered_paths(PlannerInfo *root,
input_path->pathkeys, &presorted_keys);
if (is_sorted)
- sorted_path = input_path;
+ sorted_path = copy_path(input_path);
else
{
/*
diff --git a/src/backend/optimizer/util/pathnode.c
b/src/backend/optimizer/util/pathnode.c
index 73518c8f870..5eae724db80 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -237,6 +237,161 @@ compare_path_costs_fuzzily(Path *path1, Path *path2,
double fuzz_factor)
#undef CONSIDER_PATH_STARTUP_COST
}
+/*
+ * copy_path
+ * Make a shallow copy of a Path node.
+ *
+ * The new node deliberately retains path->parent. The parent field is not an
+ * ownership marker — it is a stable identity link. For example, it is used in
+ * createplan.c to identify base relation.
+ *
+ * For T_CustomPath, we dispatch to the optional CopyCustomPath callback if
+ * provided; otherwise we fall back to a sizeof(CustomPath) memcpy, which is
+ * correct only when the extension hasn't appended its own private fields.
+ *
+ * Note: this is intentionally shallower than copyObject() (which deep-copies
+ * sublists and substructure) and lighter than reparameterize_path() (which
+ * re-runs the constructor and re-costs). We need only a fresh top-level
+ * node so add_path()'s pfree and apply_projection_to_path()'s in-place
+ * mutation cannot affect the original.
+ */
+Path *
+copy_path(Path *path)
+{
+ Path *newpath;
+
+ Assert(path != NULL);
+
+#define FLAT_COPY_PATH(newnode_, node_, nodetype_) \
+ ( (newnode_) = (Path *) palloc(sizeof(nodetype_)), \
+ memcpy((newnode_), (node_), sizeof(nodetype_)) )
+
+ switch (nodeTag(path))
+ {
+ case T_Path:
+ FLAT_COPY_PATH(newpath, path, Path);
+ break;
+ case T_IndexPath:
+ FLAT_COPY_PATH(newpath, path, IndexPath);
+ break;
+ case T_BitmapHeapPath:
+ FLAT_COPY_PATH(newpath, path, BitmapHeapPath);
+ break;
+ case T_BitmapAndPath:
+ FLAT_COPY_PATH(newpath, path, BitmapAndPath);
+ break;
+ case T_BitmapOrPath:
+ FLAT_COPY_PATH(newpath, path, BitmapOrPath);
+ break;
+ case T_TidPath:
+ FLAT_COPY_PATH(newpath, path, TidPath);
+ break;
+ case T_TidRangePath:
+ FLAT_COPY_PATH(newpath, path, TidRangePath);
+ break;
+ case T_SubqueryScanPath:
+ FLAT_COPY_PATH(newpath, path, SubqueryScanPath);
+ break;
+ case T_ForeignPath:
+ FLAT_COPY_PATH(newpath, path, ForeignPath);
+ break;
+ case T_CustomPath:
+ {
+ CustomPath *cpath = (CustomPath *) path;
+
+ Assert(cpath->methods != NULL);
+ if (cpath->methods->CopyCustomPath)
+ newpath = (Path *)
cpath->methods->CopyCustomPath(cpath);
+ else
+ FLAT_COPY_PATH(newpath, path,
CustomPath);
+ }
+ break;
+ case T_AppendPath:
+ FLAT_COPY_PATH(newpath, path, AppendPath);
+ break;
+ case T_MergeAppendPath:
+ FLAT_COPY_PATH(newpath, path, MergeAppendPath);
+ break;
+ case T_GroupResultPath:
+ FLAT_COPY_PATH(newpath, path, GroupResultPath);
+ break;
+ case T_MaterialPath:
+ FLAT_COPY_PATH(newpath, path, MaterialPath);
+ break;
+ case T_MemoizePath:
+ FLAT_COPY_PATH(newpath, path, MemoizePath);
+ break;
+ case T_GatherPath:
+ FLAT_COPY_PATH(newpath, path, GatherPath);
+ break;
+ case T_GatherMergePath:
+ FLAT_COPY_PATH(newpath, path, GatherMergePath);
+ break;
+ case T_NestPath:
+ FLAT_COPY_PATH(newpath, path, NestPath);
+ break;
+ case T_MergePath:
+ FLAT_COPY_PATH(newpath, path, MergePath);
+ break;
+ case T_HashPath:
+ FLAT_COPY_PATH(newpath, path, HashPath);
+ break;
+ case T_ProjectionPath:
+ FLAT_COPY_PATH(newpath, path, ProjectionPath);
+ break;
+ case T_ProjectSetPath:
+ FLAT_COPY_PATH(newpath, path, ProjectSetPath);
+ break;
+ case T_SortPath:
+ FLAT_COPY_PATH(newpath, path, SortPath);
+ break;
+ case T_IncrementalSortPath:
+ FLAT_COPY_PATH(newpath, path, IncrementalSortPath);
+ break;
+ case T_GroupPath:
+ FLAT_COPY_PATH(newpath, path, GroupPath);
+ break;
+ case T_UniquePath:
+ FLAT_COPY_PATH(newpath, path, UniquePath);
+ break;
+ case T_AggPath:
+ FLAT_COPY_PATH(newpath, path, AggPath);
+ break;
+ case T_GroupingSetsPath:
+ FLAT_COPY_PATH(newpath, path, GroupingSetsPath);
+ break;
+ case T_MinMaxAggPath:
+ FLAT_COPY_PATH(newpath, path, MinMaxAggPath);
+ break;
+ case T_WindowAggPath:
+ FLAT_COPY_PATH(newpath, path, WindowAggPath);
+ break;
+ case T_SetOpPath:
+ FLAT_COPY_PATH(newpath, path, SetOpPath);
+ break;
+ case T_RecursiveUnionPath:
+ FLAT_COPY_PATH(newpath, path, RecursiveUnionPath);
+ break;
+ case T_LockRowsPath:
+ FLAT_COPY_PATH(newpath, path, LockRowsPath);
+ break;
+ case T_ModifyTablePath:
+ FLAT_COPY_PATH(newpath, path, ModifyTablePath);
+ break;
+ case T_LimitPath:
+ FLAT_COPY_PATH(newpath, path, LimitPath);
+ break;
+ default:
+ elog(ERROR, "unrecognized path type: %d", (int)
nodeTag(path));
+ newpath = NULL; /* keep compiler quiet */
+ break;
+ }
+
+#undef FLAT_COPY_PATH
+
+ return newpath;
+}
+
/*
* set_cheapest
* Find the minimum-cost paths from among a relation's paths,
@@ -2678,7 +2833,8 @@ create_projection_path(PlannerInfo *root,
* a separate Result plan node isn't needed, we just replace the given path's
* pathtarget with the desired one. This must be used only when the caller
* knows that the given path isn't referenced elsewhere and so can be modified
- * in-place.
+ * in-place. In particular, callers must not pass a Path that is currently
+ * reachable from another RelOptInfo's pathlist.
*
* If the input path is a GatherPath or GatherMergePath, we try to push the
* new target down to its input as well; this is a yet more invasive
diff --git a/src/include/nodes/extensible.h b/src/include/nodes/extensible.h
index 517db95c4a3..762b09976f5 100644
--- a/src/include/nodes/extensible.h
+++ b/src/include/nodes/extensible.h
@@ -103,6 +103,13 @@ typedef struct CustomPathMethods
struct List *(*ReparameterizeCustomPathByChild) (PlannerInfo *root,
List *custom_private,
RelOptInfo *child_rel);
+
+ /*
+ * Produce a shallow copy of a CustomPath, returning a freshly palloc'd
+ * struct of the extension's concrete type. Required when the
extension's
+ * CustomPath subclass embeds private fields beyond sizeof(CustomPath).
+ */
+ struct CustomPath *(*CopyCustomPath) (struct CustomPath *path);
} CustomPathMethods;
/*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index e8db321f92b..fbafdfd1e6f 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -55,6 +55,7 @@ extern int compare_path_costs(Path *path1, Path *path2,
extern int compare_fractional_path_costs(Path *path1, Path *path2,
double fraction);
extern void set_cheapest(RelOptInfo *parent_rel);
+extern Path *copy_path(Path *path);
extern void add_path(RelOptInfo *parent_rel, Path *new_path);
extern bool add_path_precheck(RelOptInfo *parent_rel, int disabled_nodes,
Cost startup_cost,
Cost total_cost,
--
2.54.0