public inbox for [email protected]  
help / color / mirror / Atom feed
BUG #19484: Segmentation fault triggered by FDW
10+ messages / 6 participants
[nested] [flat]

* BUG #19484: Segmentation fault triggered by FDW
@ 2026-05-18 06:38 PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  0 siblings, 1 reply; 10+ messages in thread

From: PG Bug reporting form @ 2026-05-18 06:38 UTC (permalink / raw)
  To: [email protected]; +Cc: [email protected]

The following bug has been logged on the website:

Bug reference:      19484
Logged by:          Chi Zhang
Email address:      [email protected]
PostgreSQL version: 18.4
Operating system:   Ubuntu 24.04
Description:        

Hi,

I found the following test case triggers a segmentation fault:

```
\set ON_ERROR_STOP on

CREATE EXTENSION postgres_fdw;

CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (
  host '/path/to/pg_socket',
  port '5432',
  dbname :'dbname'
);

CREATE USER MAPPING FOR postgres SERVER loopback
OPTIONS (user 'postgres');

CREATE SCHEMA r;
CREATE TABLE r.remote_p2 (a int NOT NULL, b int);

CREATE TABLE pt (a int NOT NULL, b int) PARTITION BY LIST (a);
CREATE TABLE pt_p1 PARTITION OF pt FOR VALUES IN (1);
CREATE FOREIGN TABLE pt_p2 PARTITION OF pt FOR VALUES IN (2)
  SERVER loopback
  OPTIONS (schema_name 'r', table_name 'remote_p2');

INSERT INTO pt_p1 VALUES (1, 10);
INSERT INTO r.remote_p2 VALUES (2, 20);

SET plan_cache_mode = force_generic_plan;

PREPARE upd(int) AS
  UPDATE pt
  SET b = b + 1
  WHERE a = $1
  RETURNING tableoid::regclass, a, b;

EXPLAIN (costs off) EXECUTE upd(2);
EXECUTE upd(2);
SELECT * FROM r.remote_p2 ORDER BY a;

```

This is the log:

```
2026-05-18 13:40:41.888 CST [21729] LOG:  database system is ready to accept
connections
  2026-05-18 13:41:03.317 CST [21932] LOG:  unexpected EOF on client
connection with an open transaction
  2026-05-18 13:41:03.317 CST [21729] LOG:  client backend (PID 21931) was
terminated by signal 11: Segmentation fault
  2026-05-18 13:41:03.317 CST [21729] DETAIL:  Failed process was running:
EXECUTE upd(2);
  2026-05-18 13:41:03.317 CST [21729] LOG:  terminating any other active
server processes
  2026-05-18 13:41:03.319 CST [21729] LOG:  all server processes terminated;
reinitializing
  2026-05-18 13:41:03.345 CST [21936] LOG:  database system was interrupted;
last known up at 2026-05-18 13:40:41 CST
  2026-05-18 13:41:03.509 CST [21936] LOG:  database system was not properly
shut down; automatic recovery in progress
  2026-05-18 13:41:03.513 CST [21936] LOG:  redo starts at 0/98371040
  2026-05-18 13:41:03.531 CST [21936] LOG:  invalid record length at
0/987B6E68: expected at least 24, got 0
  2026-05-18 13:41:03.531 CST [21936] LOG:  redo done at 0/987B6E40 system
usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.01 s
  2026-05-18 13:41:03.537 CST [21937] LOG:  checkpoint starting:
end-of-recovery fast wait
  2026-05-18 13:41:03.654 CST [21937] LOG:  checkpoint complete:
end-of-recovery fast wait: wrote 975 buffers (6.0%), wrote 3 SLRU buffers; 0
WAL file(s) added, 0 removed, 0
recycled; write=0.081 s, sync=0.030 s, total=0.121 s; sync files=325,
longest=0.005 s, average=0.001 s; distance=4375 kB, estimate=4375 kB;
lsn=0/987B6E68, redo lsn=0/987B6E68
  2026-05-18 13:41:03.660 CST [21729] LOG:  database system is ready to
accept connections
```

I built the Postgres from source code
901ed9b352b41f034e17bc540725082a488fce31 of github commit.








^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
@ 2026-05-20 12:37 ` Ayush Tiwari <[email protected]>
  2026-05-20 17:46   ` Re: BUG #19484: Segmentation fault triggered by FDW Etsuro Fujita <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  0 siblings, 2 replies; 10+ messages in thread

From: Ayush Tiwari @ 2026-05-20 12:37 UTC (permalink / raw)
  To: [email protected]; [email protected]; Etsuro Fujita <[email protected]>

Hi,

On Wed, 20 May 2026 at 03:59, PG Bug reporting form <[email protected]>
wrote:

> The following bug has been logged on the website:
>
> Bug reference:      19484
> Logged by:          Chi Zhang
> Email address:      [email protected]
> PostgreSQL version: 18.4
> Operating system:   Ubuntu 24.04
> Description:
>
> Hi,
>
> I found the following test case triggers a segmentation fault:
>
> ```
> \set ON_ERROR_STOP on
>
> CREATE EXTENSION postgres_fdw;
>
> CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw
> OPTIONS (
>   host '/path/to/pg_socket',
>   port '5432',
>   dbname :'dbname'
> );
>
> CREATE USER MAPPING FOR postgres SERVER loopback
> OPTIONS (user 'postgres');
>
> CREATE SCHEMA r;
> CREATE TABLE r.remote_p2 (a int NOT NULL, b int);
>
> CREATE TABLE pt (a int NOT NULL, b int) PARTITION BY LIST (a);
> CREATE TABLE pt_p1 PARTITION OF pt FOR VALUES IN (1);
> CREATE FOREIGN TABLE pt_p2 PARTITION OF pt FOR VALUES IN (2)
>   SERVER loopback
>   OPTIONS (schema_name 'r', table_name 'remote_p2');
>
> INSERT INTO pt_p1 VALUES (1, 10);
> INSERT INTO r.remote_p2 VALUES (2, 20);
>
> SET plan_cache_mode = force_generic_plan;
>
> PREPARE upd(int) AS
>   UPDATE pt
>   SET b = b + 1
>   WHERE a = $1
>   RETURNING tableoid::regclass, a, b;
>
> EXPLAIN (costs off) EXECUTE upd(2);
> EXECUTE upd(2);
> SELECT * FROM r.remote_p2 ORDER BY a;
>

Thanks for the very precise repro, that made this easy to track down.

I reproduced the crash on master.  The plan EXPLAIN under
force_generic_plan shows runtime pruning is in effect:

  Update on pt
    Foreign Update on pt_p2 pt_2
    ->  Append
          Subplans Removed: 1
          ->  Foreign Update on pt_p2 pt_2

The SEGV happens inside postgresBeginForeignModify() because
ExecInitModifyTable() builds re-indexed "kept" copies of several
parallel per-result-relation lists after dropping pruned relations -
withCheckOptionLists, returningLists, updateColnosLists,
mergeActionLists and mergeJoinConditions, however two members were
missed:

  - node->fdwPrivLists, read with list_nth(node->fdwPrivLists, i) when
    BeginForeignModify() is called, and
  - node->fdwDirectModifyPlans, checked with bms_is_member(i, ...) when
    setting ri_usesFdwDirectModify.

Both were still indexed against the original (pre-pruning) positions
while the surrounding loop's "i" is now the kept position.  When the
foreign partition's kept-index no longer matched its original index,
BeginForeignModify() got the wrong fdw_private and crashed.

Attached patch builds re-indexed kept copies for these two arrays in
the same loop as the other parallel lists, and uses them at the two
call sites.

Regards,
Ayush


Attachments:

  [application/octet-stream] v1-0001-Re-index-ModifyTable-FDW-arrays-when-pruning-resu.patch (7.3K, 3-v1-0001-Re-index-ModifyTable-FDW-arrays-when-pruning-resu.patch)
  download | inline diff:
From 1bcf981c29f54b77a07c25a7b3eb06d90164bd8a Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <[email protected]>
Date: Wed, 20 May 2026 05:06:57 +0000
Subject: [PATCH v1] Re-index ModifyTable FDW arrays when pruning result
 relations

ExecInitModifyTable() copies parallel per-result-relation lists from
the plan node into a new "kept" set after dropping pruned result
relations.  That re-indexing was already done for withCheckOptionLists,
returningLists, updateColnosLists, mergeActionLists and
mergeJoinConditions, but two members were missed:

  * node->fdwPrivLists, indexed by list_nth() when calling
    BeginForeignModify(), and
  * node->fdwDirectModifyPlans, indexed by bms_is_member() when setting
    ri_usesFdwDirectModify.

Both were still read using the *kept* loop variable i against the
*original* (pre-pruning) indexing, so on a partitioned UPDATE/DELETE
that uses a generic plan (PREPARE/EXECUTE under plan_cache_mode =
force_generic_plan) and runtime partition pruning, a foreign partition
whose original index no longer matched its kept position caused
BeginForeignModify() to receive the wrong fdw_private and segfault
inside the FDW.

Build re-indexed kept copies for these two arrays in the same loop as
the other parallel lists and use them at the call sites.  Add a
postgres_fdw regression case using PREPARE/EXECUTE under
force_generic_plan that exercises the failing path.

Reported-by: Chi Zhang <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
 .../postgres_fdw/expected/postgres_fdw.out    | 26 +++++++++++++++++++
 contrib/postgres_fdw/sql/postgres_fdw.sql     | 22 ++++++++++++++++
 src/backend/executor/nodeModifyTable.c        | 18 +++++++++++--
 3 files changed, 64 insertions(+), 2 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..872d871a675 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9337,6 +9337,32 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        returning tableoid::regclass, a, b;
+execute fdw_part_upd(2);
+      tableoid      | a | b  
+--------------------+---+----
+ fdw_part_update_p2 | 2 | 21
+(1 row)
+
+deallocate fdw_part_upd;
+reset plan_cache_mode;
+drop table fdw_part_update;
+drop table fdw_part_update_remote;
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..c80aaf1c1b4 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2723,6 +2723,28 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        returning tableoid::regclass, a, b;
+execute fdw_part_upd(2);
+deallocate fdw_part_upd;
+reset plan_cache_mode;
+drop table fdw_part_update;
+drop table fdw_part_update_remote;
+
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 478cb01783c..f69060cb5ab 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5108,6 +5108,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	List	   *resultRelations = NIL;
 	List	   *withCheckOptionLists = NIL;
 	List	   *returningLists = NIL;
+	/* fdwPrivLists/fdwDirectModifyPlans are re-indexed to match resultRelations */
+	List	   *fdwPrivLists = NIL;
+	Bitmapset  *fdwDirectModifyPlans = NULL;
 	List	   *updateColnosLists = NIL;
 	List	   *mergeActionLists = NIL;
 	List	   *mergeJoinConditions = NIL;
@@ -5153,6 +5156,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		if (keep_rel)
 		{
+			int			new_index = list_length(resultRelations);
+
 			resultRelations = lappend_int(resultRelations, rti);
 			if (node->withCheckOptionLists)
 			{
@@ -5170,6 +5175,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 				returningLists = lappend(returningLists, returningList);
 			}
+			if (node->fdwPrivLists)
+			{
+				List	   *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i);
+
+				fdwPrivLists = lappend(fdwPrivLists, fdwPrivList);
+			}
+			if (bms_is_member(i, node->fdwDirectModifyPlans))
+				fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans,
+													  new_index);
 			if (node->updateColnosLists)
 			{
 				List	   *updateColnosList = list_nth(node->updateColnosLists, i);
@@ -5291,7 +5305,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		/* Initialize the usesFdwDirectModify flag */
 		resultRelInfo->ri_usesFdwDirectModify =
-			bms_is_member(i, node->fdwDirectModifyPlans);
+			bms_is_member(i, fdwDirectModifyPlans);
 
 		/*
 		 * Verify result relation is a valid target for the current operation
@@ -5320,7 +5334,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_FdwRoutine != NULL &&
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
 		{
-			List	   *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
+			List	   *fdw_private = (List *) list_nth(fdwPrivLists, i);
 
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
 															 resultRelInfo,
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
@ 2026-05-20 17:46   ` Etsuro Fujita <[email protected]>
  1 sibling, 0 replies; 10+ messages in thread

From: Etsuro Fujita @ 2026-05-20 17:46 UTC (permalink / raw)
  To: Ayush Tiwari <[email protected]>; +Cc: [email protected]; [email protected]

Hi,

On Wed, May 20, 2026 at 5:37 AM Ayush Tiwari
<[email protected]> wrote:
> On Wed, 20 May 2026 at 03:59, PG Bug reporting form <[email protected]> wrote:
>> I found the following test case triggers a segmentation fault:

[snip]

> Thanks for the very precise repro, that made this easy to track down.
>
> I reproduced the crash on master.  The plan EXPLAIN under
> force_generic_plan shows runtime pruning is in effect:
>
>   Update on pt
>     Foreign Update on pt_p2 pt_2
>     ->  Append
>           Subplans Removed: 1
>           ->  Foreign Update on pt_p2 pt_2
>
> The SEGV happens inside postgresBeginForeignModify() because
> ExecInitModifyTable() builds re-indexed "kept" copies of several
> parallel per-result-relation lists after dropping pruned relations -
> withCheckOptionLists, returningLists, updateColnosLists,
> mergeActionLists and mergeJoinConditions, however two members were
> missed:
>
>   - node->fdwPrivLists, read with list_nth(node->fdwPrivLists, i) when
>     BeginForeignModify() is called, and
>   - node->fdwDirectModifyPlans, checked with bms_is_member(i, ...) when
>     setting ri_usesFdwDirectModify.
>
> Both were still indexed against the original (pre-pruning) positions
> while the surrounding loop's "i" is now the kept position.  When the
> foreign partition's kept-index no longer matched its original index,
> BeginForeignModify() got the wrong fdw_private and crashed.
>
> Attached patch builds re-indexed kept copies for these two arrays in
> the same loop as the other parallel lists, and uses them at the two
> call sites.

Thanks Chi for the report, and Ayush for the analysis and patch!  Will review.

Best regards,
Etsuro Fujita






^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
@ 2026-05-22 20:56   ` Matheus Alcantara <[email protected]>
  2026-05-30 06:18     ` Re: BUG #19484: Segmentation fault triggered by FDW Rafia Sabih <[email protected]>
  1 sibling, 1 reply; 10+ messages in thread

From: Matheus Alcantara @ 2026-05-22 20:56 UTC (permalink / raw)
  To: Ayush Tiwari <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>

On Wed May 20, 2026 at 9:37 AM -03, Ayush Tiwari wrote:
> I reproduced the crash on master.  The plan EXPLAIN under
> force_generic_plan shows runtime pruning is in effect:
>
>   Update on pt
>     Foreign Update on pt_p2 pt_2
>     ->  Append
>           Subplans Removed: 1
>           ->  Foreign Update on pt_p2 pt_2
>
> The SEGV happens inside postgresBeginForeignModify() because
> ExecInitModifyTable() builds re-indexed "kept" copies of several
> parallel per-result-relation lists after dropping pruned relations -
> withCheckOptionLists, returningLists, updateColnosLists,
> mergeActionLists and mergeJoinConditions, however two members were
> missed:
>
>   - node->fdwPrivLists, read with list_nth(node->fdwPrivLists, i) when
>     BeginForeignModify() is called, and
>   - node->fdwDirectModifyPlans, checked with bms_is_member(i, ...) when
>     setting ri_usesFdwDirectModify.
>
> Both were still indexed against the original (pre-pruning) positions
> while the surrounding loop's "i" is now the kept position.  When the
> foreign partition's kept-index no longer matched its original index,
> BeginForeignModify() got the wrong fdw_private and crashed.
>
> Attached patch builds re-indexed kept copies for these two arrays in
> the same loop as the other parallel lists, and uses them at the two
> call sites.
>

Hi, thanks for the patch. This issue started on version 18 by commit
cbc127917e0.

The patch fixes the issue and it make sense to me. One a minor comment
is that I think pg_indent is needed on nodeModifyTable.c

--
Matheus Alcantara
EDB: https://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
@ 2026-05-30 06:18     ` Rafia Sabih <[email protected]>
  2026-06-09 15:10       ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  0 siblings, 1 reply; 10+ messages in thread

From: Rafia Sabih @ 2026-05-30 06:18 UTC (permalink / raw)
  To: Matheus Alcantara <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>

On Fri, 22 May 2026 at 22:56, Matheus Alcantara <[email protected]>
wrote:

> On Wed May 20, 2026 at 9:37 AM -03, Ayush Tiwari wrote:
> > I reproduced the crash on master.  The plan EXPLAIN under
> > force_generic_plan shows runtime pruning is in effect:
> >
> >   Update on pt
> >     Foreign Update on pt_p2 pt_2
> >     ->  Append
> >           Subplans Removed: 1
> >           ->  Foreign Update on pt_p2 pt_2
> >
> > The SEGV happens inside postgresBeginForeignModify() because
> > ExecInitModifyTable() builds re-indexed "kept" copies of several
> > parallel per-result-relation lists after dropping pruned relations -
> > withCheckOptionLists, returningLists, updateColnosLists,
> > mergeActionLists and mergeJoinConditions, however two members were
> > missed:
> >
> >   - node->fdwPrivLists, read with list_nth(node->fdwPrivLists, i) when
> >     BeginForeignModify() is called, and
> >   - node->fdwDirectModifyPlans, checked with bms_is_member(i, ...) when
> >     setting ri_usesFdwDirectModify.
> >
> > Both were still indexed against the original (pre-pruning) positions
> > while the surrounding loop's "i" is now the kept position.  When the
> > foreign partition's kept-index no longer matched its original index,
> > BeginForeignModify() got the wrong fdw_private and crashed.
> >
> > Attached patch builds re-indexed kept copies for these two arrays in
> > the same loop as the other parallel lists, and uses them at the two
> > call sites.
> >
>
A good catch. However there is one issue that remains here,
in show_modifytable_info still is using the old index here fdw_private =
(List *) list_nth(node->fdwPrivLists, j) i.e. the one before pruning.
In fact I found a scenario where it is causing crash, try this

create table fdw_part_update2 (a int not null, b int) partition by list (a);
create table fdw_part_update2_p1 partition of fdw_part_update2 for values
in (1);
create table fdw_part_update2_remote (a int not null, b int);
create foreign table fdw_part_update2_p2 partition of fdw_part_update2
    for values in (2)
    server loopback options (table_name 'fdw_part_update2_remote');
insert into fdw_part_update2_p1 values (1, 10);
insert into fdw_part_update2_remote values (2, 20);
set plan_cache_mode = force_generic_plan;
 prepare fdw_part_upd2(int) as
      update fdw_part_update2 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);

Please find the attached file for the patch to fix this. This patch applies
over the earlier patch (given by Ayush) in this thread.

>
> Hi, thanks for the patch. This issue started on version 18 by commit
> cbc127917e0.
>
> The patch fixes the issue and it make sense to me. One a minor comment
> is that I think pg_indent is needed on nodeModifyTable.c
>
> --
> Matheus Alcantara
> EDB: https://www.enterprisedb.com
>
>
>

-- 
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH


Attachments:

  [application/octet-stream] 0001-Fix-show_modifytable_info.patch (5.7K, 3-0001-Fix-show_modifytable_info.patch)
  download | inline diff:
From 984ff4240463ae8627e734351afa4f9d131162dd Mon Sep 17 00:00:00 2001
From: Rafia Sabih <[email protected]>
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)



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-05-30 06:18     ` Re: BUG #19484: Segmentation fault triggered by FDW Rafia Sabih <[email protected]>
@ 2026-06-09 15:10       ` Matheus Alcantara <[email protected]>
  2026-06-10 05:15         ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  0 siblings, 1 reply; 10+ messages in thread

From: Matheus Alcantara @ 2026-06-09 15:10 UTC (permalink / raw)
  To: Rafia Sabih <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>

On Sat May 30, 2026 at 3:18 AM -03, Rafia Sabih wrote:
> A good catch. However there is one issue that remains here,
> in show_modifytable_info still is using the old index here fdw_private =
> (List *) list_nth(node->fdwPrivLists, j) i.e. the one before pruning.
> In fact I found a scenario where it is causing crash, try this
>
> create table fdw_part_update2 (a int not null, b int) partition by list (a);
> create table fdw_part_update2_p1 partition of fdw_part_update2 for values
> in (1);
> create table fdw_part_update2_remote (a int not null, b int);
> create foreign table fdw_part_update2_p2 partition of fdw_part_update2
>     for values in (2)
>     server loopback options (table_name 'fdw_part_update2_remote');
> insert into fdw_part_update2_p1 values (1, 10);
> insert into fdw_part_update2_remote values (2, 20);
> set plan_cache_mode = force_generic_plan;
>  prepare fdw_part_upd2(int) as
>       update fdw_part_update2 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);
>
> Please find the attached file for the patch to fix this. This patch applies
> over the earlier patch (given by Ayush) in this thread.
>

Thanks for catching this, Rafia. The fix is correct —
show_modifytable_info() was indeed still reading from node->fdwPrivLists
using the post-pruning index j, which causes an out-of-bounds access
when partitions are pruned.

I think both patches should be squashed into a single one since they fix
the same underlying issue. I've done this locally and also ran pg_indent
over the result. Attached is the combined patch.

One minor naming observation: the new fdwPrivLists field in
ModifyTableState doesn't follow the mt_ prefix convention used by the
other re-indexed lists (mt_updateColnosLists, mt_mergeActionLists,
mt_mergeJoinConditions). Should we rename it to mt_fdwPrivLists for
consistency?

--
Matheus Alcantara
EDB: https://www.enterprisedb.com

From 4020e86e2ec85b6fd58397b5f4f467d1baf5ad87 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Tue, 9 Jun 2026 11:55:34 -0300
Subject: [PATCH] Re-index ModifyTable FDW arrays when pruning result relations

ExecInitModifyTable() copies parallel per-result-relation lists from
the plan node into a new "kept" set after dropping pruned result
relations. That re-indexing was already done for withCheckOptionLists,
returningLists, updateColnosLists, mergeActionLists and
mergeJoinConditions, but fdwPrivLists and fdwDirectModifyPlans were missed.

Additionally, show_modifytable_info() in explain.c was reading the
plan-indexed node->fdwPrivLists using the post-pruning executor index,
causing out-of-bounds access. Fix by saving the re-indexed list to
mtstate->fdwPrivLists and reading from there.

Author: Ayush Tiwari <[email protected]>
Author: Rafia Sabih <[email protected]>
Co-authored-by: Matheus Alcantara <[email protected]>
Reported-by: Chi Zhang <[email protected]>
Discussion: https://www.postgresql.org/message-id/19484-a3cb82c8cde3c8fa%40postgresql.org
---
 .../postgres_fdw/expected/postgres_fdw.out    | 51 +++++++++++++++++++
 contrib/postgres_fdw/sql/postgres_fdw.sql     | 29 +++++++++++
 src/backend/commands/explain.c                |  2 +-
 src/backend/executor/nodeModifyTable.c        | 23 ++++++++-
 src/include/nodes/execnodes.h                 |  6 +++
 5 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..5f5cb78ee65 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9337,6 +9337,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        returning tableoid::regclass, a, b;
+execute fdw_part_upd(2);
+      tableoid      | a | b  
+--------------------+---+----
+ fdw_part_update_p2 | 2 | 21
+(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;
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..dc135573a21 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2723,6 +2723,35 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        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;
+
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..3e43c97896e 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 33a6735f08d..a631c345c5e 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5105,6 +5105,13 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	List	   *resultRelations = NIL;
 	List	   *withCheckOptionLists = NIL;
 	List	   *returningLists = NIL;
+
+	/*
+	 * fdwPrivLists/fdwDirectModifyPlans are re-indexed to match
+	 * resultRelations
+	 */
+	List	   *fdwPrivLists = NIL;
+	Bitmapset  *fdwDirectModifyPlans = NULL;
 	List	   *updateColnosLists = NIL;
 	List	   *mergeActionLists = NIL;
 	List	   *mergeJoinConditions = NIL;
@@ -5150,6 +5157,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		if (keep_rel)
 		{
+			int			new_index = list_length(resultRelations);
+
 			resultRelations = lappend_int(resultRelations, rti);
 			if (node->withCheckOptionLists)
 			{
@@ -5167,6 +5176,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 				returningLists = lappend(returningLists, returningList);
 			}
+			if (node->fdwPrivLists)
+			{
+				List	   *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i);
+
+				fdwPrivLists = lappend(fdwPrivLists, fdwPrivList);
+			}
+			if (bms_is_member(i, node->fdwDirectModifyPlans))
+				fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans,
+													  new_index);
 			if (node->updateColnosLists)
 			{
 				List	   *updateColnosList = list_nth(node->updateColnosLists, i);
@@ -5213,6 +5231,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:
@@ -5288,7 +5307,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		/* Initialize the usesFdwDirectModify flag */
 		resultRelInfo->ri_usesFdwDirectModify =
-			bms_is_member(i, node->fdwDirectModifyPlans);
+			bms_is_member(i, fdwDirectModifyPlans);
 
 		/*
 		 * Verify result relation is a valid target for the current operation
@@ -5317,7 +5336,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_FdwRoutine != NULL &&
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
 		{
-			List	   *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
+			List	   *fdw_private = (List *) list_nth(fdwPrivLists, i);
 
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
 															 resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 53c138310db..f64c2cc5f34 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1446,6 +1446,12 @@ typedef struct ModifyTableState
 	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.50.1 (Apple Git-155)



Attachments:

  [text/plain] 0001-Re-index-ModifyTable-FDW-arrays-when-pruning-result-.patch (10.3K, 2-0001-Re-index-ModifyTable-FDW-arrays-when-pruning-result-.patch)
  download | inline diff:
From 4020e86e2ec85b6fd58397b5f4f467d1baf5ad87 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Tue, 9 Jun 2026 11:55:34 -0300
Subject: [PATCH] Re-index ModifyTable FDW arrays when pruning result relations

ExecInitModifyTable() copies parallel per-result-relation lists from
the plan node into a new "kept" set after dropping pruned result
relations. That re-indexing was already done for withCheckOptionLists,
returningLists, updateColnosLists, mergeActionLists and
mergeJoinConditions, but fdwPrivLists and fdwDirectModifyPlans were missed.

Additionally, show_modifytable_info() in explain.c was reading the
plan-indexed node->fdwPrivLists using the post-pruning executor index,
causing out-of-bounds access. Fix by saving the re-indexed list to
mtstate->fdwPrivLists and reading from there.

Author: Ayush Tiwari <[email protected]>
Author: Rafia Sabih <[email protected]>
Co-authored-by: Matheus Alcantara <[email protected]>
Reported-by: Chi Zhang <[email protected]>
Discussion: https://www.postgresql.org/message-id/19484-a3cb82c8cde3c8fa%40postgresql.org
---
 .../postgres_fdw/expected/postgres_fdw.out    | 51 +++++++++++++++++++
 contrib/postgres_fdw/sql/postgres_fdw.sql     | 29 +++++++++++
 src/backend/commands/explain.c                |  2 +-
 src/backend/executor/nodeModifyTable.c        | 23 ++++++++-
 src/include/nodes/execnodes.h                 |  6 +++
 5 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..5f5cb78ee65 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9337,6 +9337,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        returning tableoid::regclass, a, b;
+execute fdw_part_upd(2);
+      tableoid      | a | b  
+--------------------+---+----
+ fdw_part_update_p2 | 2 | 21
+(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;
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..dc135573a21 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2723,6 +2723,35 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        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;
+
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..3e43c97896e 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 33a6735f08d..a631c345c5e 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5105,6 +5105,13 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	List	   *resultRelations = NIL;
 	List	   *withCheckOptionLists = NIL;
 	List	   *returningLists = NIL;
+
+	/*
+	 * fdwPrivLists/fdwDirectModifyPlans are re-indexed to match
+	 * resultRelations
+	 */
+	List	   *fdwPrivLists = NIL;
+	Bitmapset  *fdwDirectModifyPlans = NULL;
 	List	   *updateColnosLists = NIL;
 	List	   *mergeActionLists = NIL;
 	List	   *mergeJoinConditions = NIL;
@@ -5150,6 +5157,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		if (keep_rel)
 		{
+			int			new_index = list_length(resultRelations);
+
 			resultRelations = lappend_int(resultRelations, rti);
 			if (node->withCheckOptionLists)
 			{
@@ -5167,6 +5176,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 				returningLists = lappend(returningLists, returningList);
 			}
+			if (node->fdwPrivLists)
+			{
+				List	   *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i);
+
+				fdwPrivLists = lappend(fdwPrivLists, fdwPrivList);
+			}
+			if (bms_is_member(i, node->fdwDirectModifyPlans))
+				fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans,
+													  new_index);
 			if (node->updateColnosLists)
 			{
 				List	   *updateColnosList = list_nth(node->updateColnosLists, i);
@@ -5213,6 +5231,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:
@@ -5288,7 +5307,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		/* Initialize the usesFdwDirectModify flag */
 		resultRelInfo->ri_usesFdwDirectModify =
-			bms_is_member(i, node->fdwDirectModifyPlans);
+			bms_is_member(i, fdwDirectModifyPlans);
 
 		/*
 		 * Verify result relation is a valid target for the current operation
@@ -5317,7 +5336,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_FdwRoutine != NULL &&
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
 		{
-			List	   *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
+			List	   *fdw_private = (List *) list_nth(fdwPrivLists, i);
 
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
 															 resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 53c138310db..f64c2cc5f34 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1446,6 +1446,12 @@ typedef struct ModifyTableState
 	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.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-05-30 06:18     ` Re: BUG #19484: Segmentation fault triggered by FDW Rafia Sabih <[email protected]>
  2026-06-09 15:10       ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
@ 2026-06-10 05:15         ` Ayush Tiwari <[email protected]>
  2026-06-10 11:08           ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  0 siblings, 1 reply; 10+ messages in thread

From: Ayush Tiwari @ 2026-06-10 05:15 UTC (permalink / raw)
  To: Matheus Alcantara <[email protected]>; +Cc: Rafia Sabih <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>

Hi,

On Tue, 9 Jun 2026 at 20:40, Matheus Alcantara <[email protected]>
wrote:

>
> I think both patches should be squashed into a single one since they fix
> the same underlying issue. I've done this locally and also ran pg_indent
> over the result. Attached is the combined patch.
>

Thanks for this!


> One minor naming observation: the new fdwPrivLists field in
> ModifyTableState doesn't follow the mt_ prefix convention used by the
> other re-indexed lists (mt_updateColnosLists, mt_mergeActionLists,
> mt_mergeJoinConditions). Should we rename it to mt_fdwPrivLists for
> consistency?
>

I think yes, it makes sense to rename it.

Regards,
Ayush


^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-05-30 06:18     ` Re: BUG #19484: Segmentation fault triggered by FDW Rafia Sabih <[email protected]>
  2026-06-09 15:10       ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-06-10 05:15         ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
@ 2026-06-10 11:08           ` Matheus Alcantara <[email protected]>
  2026-06-10 13:09             ` Re: BUG #19484: Segmentation fault triggered by FDW Amit Langote <[email protected]>
  0 siblings, 1 reply; 10+ messages in thread

From: Matheus Alcantara @ 2026-06-10 11:08 UTC (permalink / raw)
  To: Ayush Tiwari <[email protected]>; +Cc: Rafia Sabih <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>; Amit Langote <[email protected]>

On Wed Jun 10, 2026 at 2:15 AM -03, Ayush Tiwari wrote:
>> One minor naming observation: the new fdwPrivLists field in
>> ModifyTableState doesn't follow the mt_ prefix convention used by the
>> other re-indexed lists (mt_updateColnosLists, mt_mergeActionLists,
>> mt_mergeJoinConditions). Should we rename it to mt_fdwPrivLists for
>> consistency?
>>
>
> I think yes, it makes sense to rename it.
>

Attached v2 renamed, thanks.

(Also CC Amit on this since he committed cbc127917e0 which I believe
that is when the issue started)

--
Matheus Alcantara
EDB: https://www.enterprisedb.com

From 971d185cf4bfb75d0459f2c7848e6b77afa2ee85 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Tue, 9 Jun 2026 11:55:34 -0300
Subject: [PATCH v2] Re-index ModifyTable FDW arrays when pruning result
 relations

ExecInitModifyTable() copies parallel per-result-relation lists from
the plan node into a new "kept" set after dropping pruned result
relations. That re-indexing was already done for withCheckOptionLists,
returningLists, updateColnosLists, mergeActionLists and
mergeJoinConditions, but fdwPrivLists and fdwDirectModifyPlans were missed.

Additionally, show_modifytable_info() in explain.c was reading the
plan-indexed node->fdwPrivLists using the post-pruning executor index,
causing out-of-bounds access. Fix by saving the re-indexed list to
mtstate->mt_fdwPrivLists and reading from there.

Author: Ayush Tiwari <[email protected]>
Author: Rafia Sabih <[email protected]>
Co-authored-by: Matheus Alcantara <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reported-by: Chi Zhang <[email protected]>
Discussion: https://www.postgresql.org/message-id/19484-a3cb82c8cde3c8fa%40postgresql.org
---
 .../postgres_fdw/expected/postgres_fdw.out    | 51 +++++++++++++++++++
 contrib/postgres_fdw/sql/postgres_fdw.sql     | 29 +++++++++++
 src/backend/commands/explain.c                |  2 +-
 src/backend/executor/nodeModifyTable.c        | 23 ++++++++-
 src/include/nodes/execnodes.h                 |  6 +++
 5 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..5f5cb78ee65 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9337,6 +9337,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        returning tableoid::regclass, a, b;
+execute fdw_part_upd(2);
+      tableoid      | a | b  
+--------------------+---+----
+ fdw_part_update_p2 | 2 | 21
+(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;
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..dc135573a21 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2723,6 +2723,35 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        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;
+
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..a40d03d35f3 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->mt_fdwPrivLists, j);
 
 			fdwroutine->ExplainForeignModify(mtstate,
 											 resultRelInfo,
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 33a6735f08d..feea28214d4 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5105,6 +5105,13 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	List	   *resultRelations = NIL;
 	List	   *withCheckOptionLists = NIL;
 	List	   *returningLists = NIL;
+
+	/*
+	 * fdwPrivLists/fdwDirectModifyPlans are re-indexed to match
+	 * resultRelations
+	 */
+	List	   *fdwPrivLists = NIL;
+	Bitmapset  *fdwDirectModifyPlans = NULL;
 	List	   *updateColnosLists = NIL;
 	List	   *mergeActionLists = NIL;
 	List	   *mergeJoinConditions = NIL;
@@ -5150,6 +5157,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		if (keep_rel)
 		{
+			int			new_index = list_length(resultRelations);
+
 			resultRelations = lappend_int(resultRelations, rti);
 			if (node->withCheckOptionLists)
 			{
@@ -5167,6 +5176,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 				returningLists = lappend(returningLists, returningList);
 			}
+			if (node->fdwPrivLists)
+			{
+				List	   *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i);
+
+				fdwPrivLists = lappend(fdwPrivLists, fdwPrivList);
+			}
+			if (bms_is_member(i, node->fdwDirectModifyPlans))
+				fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans,
+													  new_index);
 			if (node->updateColnosLists)
 			{
 				List	   *updateColnosList = list_nth(node->updateColnosLists, i);
@@ -5213,6 +5231,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	mtstate->mt_updateColnosLists = updateColnosLists;
 	mtstate->mt_mergeActionLists = mergeActionLists;
 	mtstate->mt_mergeJoinConditions = mergeJoinConditions;
+	mtstate->mt_fdwPrivLists = fdwPrivLists;
 
 	/*----------
 	 * Resolve the target relation. This is the same as:
@@ -5288,7 +5307,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		/* Initialize the usesFdwDirectModify flag */
 		resultRelInfo->ri_usesFdwDirectModify =
-			bms_is_member(i, node->fdwDirectModifyPlans);
+			bms_is_member(i, fdwDirectModifyPlans);
 
 		/*
 		 * Verify result relation is a valid target for the current operation
@@ -5317,7 +5336,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_FdwRoutine != NULL &&
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
 		{
-			List	   *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
+			List	   *fdw_private = (List *) list_nth(fdwPrivLists, i);
 
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
 															 resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 53c138310db..5871383961f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1446,6 +1446,12 @@ typedef struct ModifyTableState
 	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	   *mt_fdwPrivLists;
+
 	/*
 	 * Target relation mentioned in the original statement, used to fire
 	 * statement-level triggers and as the root for tuple routing.  (This
-- 
2.50.1 (Apple Git-155)



Attachments:

  [text/plain] v2-0001-Re-index-ModifyTable-FDW-arrays-when-pruning-resu.patch (10.4K, 2-v2-0001-Re-index-ModifyTable-FDW-arrays-when-pruning-resu.patch)
  download | inline diff:
From 971d185cf4bfb75d0459f2c7848e6b77afa2ee85 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Tue, 9 Jun 2026 11:55:34 -0300
Subject: [PATCH v2] Re-index ModifyTable FDW arrays when pruning result
 relations

ExecInitModifyTable() copies parallel per-result-relation lists from
the plan node into a new "kept" set after dropping pruned result
relations. That re-indexing was already done for withCheckOptionLists,
returningLists, updateColnosLists, mergeActionLists and
mergeJoinConditions, but fdwPrivLists and fdwDirectModifyPlans were missed.

Additionally, show_modifytable_info() in explain.c was reading the
plan-indexed node->fdwPrivLists using the post-pruning executor index,
causing out-of-bounds access. Fix by saving the re-indexed list to
mtstate->mt_fdwPrivLists and reading from there.

Author: Ayush Tiwari <[email protected]>
Author: Rafia Sabih <[email protected]>
Co-authored-by: Matheus Alcantara <[email protected]>
Reviewed-by: Ayush Tiwari <[email protected]>
Reported-by: Chi Zhang <[email protected]>
Discussion: https://www.postgresql.org/message-id/19484-a3cb82c8cde3c8fa%40postgresql.org
---
 .../postgres_fdw/expected/postgres_fdw.out    | 51 +++++++++++++++++++
 contrib/postgres_fdw/sql/postgres_fdw.sql     | 29 +++++++++++
 src/backend/commands/explain.c                |  2 +-
 src/backend/executor/nodeModifyTable.c        | 23 ++++++++-
 src/include/nodes/execnodes.h                 |  6 +++
 5 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..5f5cb78ee65 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9337,6 +9337,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        returning tableoid::regclass, a, b;
+execute fdw_part_upd(2);
+      tableoid      | a | b  
+--------------------+---+----
+ fdw_part_update_p2 | 2 | 21
+(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;
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..dc135573a21 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2723,6 +2723,35 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Runtime pruning of result relations must keep ModifyTable's per-relation
+-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept
+-- resultRelations.  Otherwise BeginForeignModify() reads the wrong
+-- fdw_private and segfaults.
+create table fdw_part_update (a int not null, b int) partition by list (a);
+create table fdw_part_update_p1 partition of fdw_part_update for values in (1);
+create table fdw_part_update_remote (a int not null, b int);
+create foreign table fdw_part_update_p2 partition of fdw_part_update
+    for values in (2)
+    server loopback options (table_name 'fdw_part_update_remote');
+insert into fdw_part_update_p1 values (1, 10);
+insert into fdw_part_update_remote values (2, 20);
+set plan_cache_mode = force_generic_plan;
+prepare fdw_part_upd(int) as
+    update fdw_part_update set b = b + 1 where a = $1
+        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;
+
 -- Test that remote triggers work with update tuple routing
 create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..a40d03d35f3 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->mt_fdwPrivLists, j);
 
 			fdwroutine->ExplainForeignModify(mtstate,
 											 resultRelInfo,
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 33a6735f08d..feea28214d4 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5105,6 +5105,13 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	List	   *resultRelations = NIL;
 	List	   *withCheckOptionLists = NIL;
 	List	   *returningLists = NIL;
+
+	/*
+	 * fdwPrivLists/fdwDirectModifyPlans are re-indexed to match
+	 * resultRelations
+	 */
+	List	   *fdwPrivLists = NIL;
+	Bitmapset  *fdwDirectModifyPlans = NULL;
 	List	   *updateColnosLists = NIL;
 	List	   *mergeActionLists = NIL;
 	List	   *mergeJoinConditions = NIL;
@@ -5150,6 +5157,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		if (keep_rel)
 		{
+			int			new_index = list_length(resultRelations);
+
 			resultRelations = lappend_int(resultRelations, rti);
 			if (node->withCheckOptionLists)
 			{
@@ -5167,6 +5176,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 				returningLists = lappend(returningLists, returningList);
 			}
+			if (node->fdwPrivLists)
+			{
+				List	   *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i);
+
+				fdwPrivLists = lappend(fdwPrivLists, fdwPrivList);
+			}
+			if (bms_is_member(i, node->fdwDirectModifyPlans))
+				fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans,
+													  new_index);
 			if (node->updateColnosLists)
 			{
 				List	   *updateColnosList = list_nth(node->updateColnosLists, i);
@@ -5213,6 +5231,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	mtstate->mt_updateColnosLists = updateColnosLists;
 	mtstate->mt_mergeActionLists = mergeActionLists;
 	mtstate->mt_mergeJoinConditions = mergeJoinConditions;
+	mtstate->mt_fdwPrivLists = fdwPrivLists;
 
 	/*----------
 	 * Resolve the target relation. This is the same as:
@@ -5288,7 +5307,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 		/* Initialize the usesFdwDirectModify flag */
 		resultRelInfo->ri_usesFdwDirectModify =
-			bms_is_member(i, node->fdwDirectModifyPlans);
+			bms_is_member(i, fdwDirectModifyPlans);
 
 		/*
 		 * Verify result relation is a valid target for the current operation
@@ -5317,7 +5336,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_FdwRoutine != NULL &&
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
 		{
-			List	   *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
+			List	   *fdw_private = (List *) list_nth(fdwPrivLists, i);
 
 			resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
 															 resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 53c138310db..5871383961f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1446,6 +1446,12 @@ typedef struct ModifyTableState
 	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	   *mt_fdwPrivLists;
+
 	/*
 	 * Target relation mentioned in the original statement, used to fire
 	 * statement-level triggers and as the root for tuple routing.  (This
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-05-30 06:18     ` Re: BUG #19484: Segmentation fault triggered by FDW Rafia Sabih <[email protected]>
  2026-06-09 15:10       ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-06-10 05:15         ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-06-10 11:08           ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
@ 2026-06-10 13:09             ` Amit Langote <[email protected]>
  2026-06-10 13:27               ` Re: BUG #19484: Segmentation fault triggered by FDW Amit Langote <[email protected]>
  0 siblings, 1 reply; 10+ messages in thread

From: Amit Langote @ 2026-06-10 13:09 UTC (permalink / raw)
  To: Matheus Alcantara <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; Rafia Sabih <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>; Amit Langote <[email protected]>

Hi Matheus,

On Wed, Jun 10, 2026 at 8:09 PM Matheus Alcantara
<[email protected]> wrote:
>
> On Wed Jun 10, 2026 at 2:15 AM -03, Ayush Tiwari wrote:
> >> One minor naming observation: the new fdwPrivLists field in
> >> ModifyTableState doesn't follow the mt_ prefix convention used by the
> >> other re-indexed lists (mt_updateColnosLists, mt_mergeActionLists,
> >> mt_mergeJoinConditions). Should we rename it to mt_fdwPrivLists for
> >> consistency?
> >>
> >
> > I think yes, it makes sense to rename it.
> >
>
> Attached v2 renamed, thanks.
>
> (Also CC Amit on this since he committed cbc127917e0 which I believe
> that is when the issue started)

Thanks for adding me.  I'll take a look at this early next week.

-- 
Thanks, Amit Langote





^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19484: Segmentation fault triggered by FDW
  2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
  2026-05-20 12:37 ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-05-22 20:56   ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-05-30 06:18     ` Re: BUG #19484: Segmentation fault triggered by FDW Rafia Sabih <[email protected]>
  2026-06-09 15:10       ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-06-10 05:15         ` Re: BUG #19484: Segmentation fault triggered by FDW Ayush Tiwari <[email protected]>
  2026-06-10 11:08           ` Re: BUG #19484: Segmentation fault triggered by FDW Matheus Alcantara <[email protected]>
  2026-06-10 13:09             ` Re: BUG #19484: Segmentation fault triggered by FDW Amit Langote <[email protected]>
@ 2026-06-10 13:27               ` Amit Langote <[email protected]>
  0 siblings, 0 replies; 10+ messages in thread

From: Amit Langote @ 2026-06-10 13:27 UTC (permalink / raw)
  To: Matheus Alcantara <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; Rafia Sabih <[email protected]>; [email protected]; [email protected]; Etsuro Fujita <[email protected]>; Amit Langote <[email protected]>

On Wed, Jun 10, 2026 at 10:09 PM Amit Langote <[email protected]> wrote:
> On Wed, Jun 10, 2026 at 8:09 PM Matheus Alcantara
> <[email protected]> wrote:
> > On Wed Jun 10, 2026 at 2:15 AM -03, Ayush Tiwari wrote:
> > >> One minor naming observation: the new fdwPrivLists field in
> > >> ModifyTableState doesn't follow the mt_ prefix convention used by the
> > >> other re-indexed lists (mt_updateColnosLists, mt_mergeActionLists,
> > >> mt_mergeJoinConditions). Should we rename it to mt_fdwPrivLists for
> > >> consistency?
> > >>
> > >
> > > I think yes, it makes sense to rename it.
> > >
> >
> > Attached v2 renamed, thanks.
> >
> > (Also CC Amit on this since he committed cbc127917e0 which I believe
> > that is when the issue started)
>
> Thanks for adding me.  I'll take a look at this early next week.

I looked, and the patch seems straightforward enough.

Before committing it, I'd like to wait briefly to see if Fujita-san
has any thoughts on the FDW-side concerns, since he has already chimed
in on the thread.


--
Thanks, Amit Langote






^ permalink  raw  reply  [nested|flat] 10+ messages in thread


end of thread, other threads:[~2026-06-10 13:27 UTC | newest]

Thread overview: 10+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-05-18 06:38 BUG #19484: Segmentation fault triggered by FDW PG Bug reporting form <[email protected]>
2026-05-20 12:37 ` Ayush Tiwari <[email protected]>
2026-05-20 17:46   ` Etsuro Fujita <[email protected]>
2026-05-22 20:56   ` Matheus Alcantara <[email protected]>
2026-05-30 06:18     ` Rafia Sabih <[email protected]>
2026-06-09 15:10       ` Matheus Alcantara <[email protected]>
2026-06-10 05:15         ` Ayush Tiwari <[email protected]>
2026-06-10 11:08           ` Matheus Alcantara <[email protected]>
2026-06-10 13:09             ` Amit Langote <[email protected]>
2026-06-10 13:27               ` Amit Langote <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox