From 984ff4240463ae8627e734351afa4f9d131162dd Mon Sep 17 00:00:00 2001 From: Rafia Sabih Date: Sat, 30 May 2026 08:11:35 +0200 Subject: [PATCH] Fix show_modifytable_info() show_modifytable_info() in explain.c reads the plan-indexed node->fdwPrivLists using the post-pruning executor index j, producing an out-of-bounds access when calling ExplainForeignModify on a non-direct-modify FDW relation. Fix by saving the re-indexed list to mtstate->fdwPrivLists (new field in ModifyTableState) and reading from there in explain.c. --- .../postgres_fdw/expected/postgres_fdw.out | 25 +++++++++++++++++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 7 ++++++ src/backend/commands/explain.c | 2 +- src/backend/executor/nodeModifyTable.c | 3 ++- src/include/nodes/execnodes.h | 3 ++- 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 872d871a675..5f5cb78ee65 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -9360,6 +9360,31 @@ execute fdw_part_upd(2); (1 row) deallocate fdw_part_upd; +prepare fdw_part_upd2(int) as + update fdw_part_update set b = b + random()::int * 0 + 1 where a = $1 + returning tableoid::regclass, a, b; +execute fdw_part_upd2(2); + tableoid | a | b +--------------------+---+---- + fdw_part_update_p2 | 2 | 22 +(1 row) + +explain (analyze, verbose, costs off, timing off, summary off) + execute fdw_part_upd2(2); + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------- + Update on public.fdw_part_update (actual rows=1.00 loops=1) + Output: (fdw_part_update_1.tableoid)::regclass, fdw_part_update_1.a, fdw_part_update_1.b + Foreign Update on public.fdw_part_update_p2 fdw_part_update_2 + Remote SQL: UPDATE public.fdw_part_update_remote SET b = $2 WHERE ctid = $1 RETURNING a, b + -> Append (actual rows=1.00 loops=1) + Subplans Removed: 1 + -> Foreign Scan on public.fdw_part_update_p2 fdw_part_update_2 (actual rows=1.00 loops=1) + Output: ((fdw_part_update_2.b + ((random())::integer * 0)) + 1), fdw_part_update_2.tableoid, fdw_part_update_2.ctid, fdw_part_update_2.* + Remote SQL: SELECT a, b, ctid FROM public.fdw_part_update_remote WHERE ((a = $1::integer)) FOR UPDATE +(9 rows) + +deallocate fdw_part_upd2; reset plan_cache_mode; drop table fdw_part_update; drop table fdw_part_update_remote; diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index c80aaf1c1b4..dc135573a21 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2741,6 +2741,13 @@ prepare fdw_part_upd(int) as returning tableoid::regclass, a, b; execute fdw_part_upd(2); deallocate fdw_part_upd; +prepare fdw_part_upd2(int) as + update fdw_part_update set b = b + random()::int * 0 + 1 where a = $1 + returning tableoid::regclass, a, b; +execute fdw_part_upd2(2); +explain (analyze, verbose, costs off, timing off, summary off) + execute fdw_part_upd2(2); +deallocate fdw_part_upd2; reset plan_cache_mode; drop table fdw_part_update; drop table fdw_part_update_remote; diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 112c17b0d64..92326291129 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -4821,7 +4821,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, fdwroutine != NULL && fdwroutine->ExplainForeignModify != NULL) { - List *fdw_private = (List *) list_nth(node->fdwPrivLists, j); + List *fdw_private = (List *) list_nth(mtstate->fdwPrivLists, j); fdwroutine->ExplainForeignModify(mtstate, resultRelInfo, diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index f69060cb5ab..a66509465b9 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -5109,7 +5109,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) List *withCheckOptionLists = NIL; List *returningLists = NIL; /* fdwPrivLists/fdwDirectModifyPlans are re-indexed to match resultRelations */ - List *fdwPrivLists = NIL; + List *fdwPrivLists = NIL; Bitmapset *fdwDirectModifyPlans = NULL; List *updateColnosLists = NIL; List *mergeActionLists = NIL; @@ -5230,6 +5230,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_updateColnosLists = updateColnosLists; mtstate->mt_mergeActionLists = mergeActionLists; mtstate->mt_mergeJoinConditions = mergeJoinConditions; + mtstate->fdwPrivLists = fdwPrivLists; /*---------- * Resolve the target relation. This is the same as: diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 13359180d25..7b33d4d0410 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1444,7 +1444,8 @@ typedef struct ModifyTableState bool mt_done; /* are we done? */ int mt_nrels; /* number of entries in resultRelInfo[] */ ResultRelInfo *resultRelInfo; /* info about target relation(s) */ - + /* Re-indexed fdw private data lists, aligned with resultRelInfo[] after pruning */ + List *fdwPrivLists; /* * Target relation mentioned in the original statement, used to fire * statement-level triggers and as the root for tuple routing. (This -- 2.39.5 (Apple Git-154)