Hi,

I discovered that it's possible to crash Postgres when using VIEWS,
FOR PORTION OF syntax and INSTEAD OF triggers together. See crash.sql.

This happens because in ExecModifyTable() around line 4827 there is no
check for `relkind == RELKIND_VIEW`. If this is the case `tupleid`
ends up being NULL which causes null pointer dereference later when
ExecDeleteEpilogue() or ExecUpdateEpilogue() calls
ExecForPortionOfLeftovers() with tupleid = NULL. An example stacktrace
is attached.

I propose fixing this by explicitly forbidding using the named
features together. See the patch.


-- 
Best regards,
Aleksander Alekseev
Program received signal SIGSEGV, Segmentation fault.
table_tuple_fetch_row_version (rel=0x7cf70db43358, tid=0x0, 
snapshot=0x5bcee69421c0 <SnapshotAnyData>, slot=0x5bcef4ff9218)
    at ../src/include/access/tableam.h:1357
1357            return rel->rd_tableam->tuple_fetch_row_version(rel, tid, 
snapshot, slot);
(gdb) bt
#0  table_tuple_fetch_row_version (rel=0x7cf70db43358, tid=0x0, 
snapshot=0x5bcee69421c0 <SnapshotAnyData>, slot=0x5bcef4ff9218)
    at ../src/include/access/tableam.h:1357
#1  0x00005bcee5f0404f in ExecForPortionOfLeftovers (context=0x7ffd5a4e5d70, 
estate=0x5bcef4f0c800, resultRelInfo=0x5bcef4f0cd90, tupleid=0x0)
    at ../src/backend/executor/nodeModifyTable.c:1475
#2  0x00005bcee5f04c21 in ExecDeleteEpilogue (context=0x7ffd5a4e5d70, 
resultRelInfo=0x5bcef4f0cd90, tupleid=0x0, oldtuple=0x7ffd5a4e5d50, 
    changingPart=false) at ../src/backend/executor/nodeModifyTable.c:1829
#3  0x00005bcee5f0525a in ExecDelete (context=0x7ffd5a4e5d70, 
resultRelInfo=0x5bcef4f0cd90, tupleid=0x0, oldtuple=0x7ffd5a4e5d50, 
    processReturning=true, changingPart=false, canSetTag=true, tmresult=0x0, 
tupleDeleted=0x0, epqreturnslot=0x0)
    at ../src/backend/executor/nodeModifyTable.c:2107
#4  0x00005bcee5f0a103 in ExecModifyTable (pstate=0x5bcef4f0cb80) at 
../src/backend/executor/nodeModifyTable.c:5002
#5  0x00005bcee5ec0515 in ExecProcNodeFirst (node=0x5bcef4f0cb80) at 
../src/backend/executor/execProcnode.c:469
#6  0x00005bcee5eb1f5a in ExecProcNode (node=0x5bcef4f0cb80) at 
../src/include/executor/executor.h:327
#7  0x00005bcee5eb5035 in ExecutePlan (queryDesc=0x5bcef4fcf4b0, 
operation=CMD_DELETE, sendTuples=false, numberTuples=0, 
    direction=ForwardScanDirection, dest=0x5bcef4fed2b8) at 
../src/backend/executor/execMain.c:1736
#8  0x00005bcee5eb2678 in standard_ExecutorRun (queryDesc=0x5bcef4fcf4b0, 
direction=ForwardScanDirection, count=0)
    at ../src/backend/executor/execMain.c:377
#9  0x00005bcee5eb24d6 in ExecutorRun (queryDesc=0x5bcef4fcf4b0, 
direction=ForwardScanDirection, count=0) at 
../src/backend/executor/execMain.c:314
#10 0x00005bcee61ae828 in ProcessQuery (plan=0x5bcef4fee0f0, 
    sourceText=0x5bcef4eb6c70 "DELETE FROM temporal_view\n    FOR PORTION OF 
valid_at FROM '2022-01-01' TO '2023-01-01'\n    WHERE id = 1;", 
    params=0x0, queryEnv=0x0, dest=0x5bcef4fed2b8, qc=0x7ffd5a4e61c0) at 
../src/backend/tcop/pquery.c:162
#11 0x00005bcee61b0391 in PortalRunMulti (portal=0x5bcef4f56ca0, 
isTopLevel=true, setHoldSnapshot=false, dest=0x5bcef4fed2b8, 
altdest=0x5bcef4fed2b8, 
    qc=0x7ffd5a4e61c0) at ../src/backend/tcop/pquery.c:1269
#12 0x00005bcee61af85b in PortalRun (portal=0x5bcef4f56ca0, 
count=9223372036854775807, isTopLevel=true, dest=0x5bcef4fed2b8, 
altdest=0x5bcef4fed2b8, 
    qc=0x7ffd5a4e61c0) at ../src/backend/tcop/pquery.c:784
#13 0x00005bcee61a7994 in exec_simple_query (
    query_string=0x5bcef4eb6c70 "DELETE FROM temporal_view\n    FOR PORTION OF 
valid_at FROM '2022-01-01' TO '2023-01-01'\n    WHERE id = 1;")
    at ../src/backend/tcop/postgres.c:1289
#14 0x00005bcee61ad69e in PostgresMain (dbname=0x5bcef4ef66e0 "eax", 
username=0x5bcef4ef66c8 "eax") at ../src/backend/tcop/postgres.c:4855
#15 0x00005bcee61a2a5a in BackendMain (startup_data=0x7ffd5a4e6470, 
startup_data_len=24) at ../src/backend/tcop/backend_startup.c:124
#16 0x00005bcee6090e04 in postmaster_child_launch (child_type=B_BACKEND, 
child_slot=1, startup_data=0x7ffd5a4e6470, startup_data_len=24, 
    client_sock=0x7ffd5a4e64d0) at 
../src/backend/postmaster/launch_backend.c:268
#17 0x00005bcee6097b88 in BackendStartup (client_sock=0x7ffd5a4e64d0) at 
../src/backend/postmaster/postmaster.c:3627
#18 0x00005bcee6094f7c in ServerLoop () at 
../src/backend/postmaster/postmaster.c:1728
#19 0x00005bcee609483f in PostmasterMain (argc=3, argv=0x5bcef4e70a00) at 
../src/backend/postmaster/postmaster.c:1415
#20 0x00005bcee5f51132 in main (argc=3, argv=0x5bcef4e70a00) at 
../src/backend/main/main.c:231

Attachment: crash.sql
Description: application/sql

From 63b6699d5c03405f36396dd98c54f53a4bcf40d3 Mon Sep 17 00:00:00 2001
From: Aleksander Alekseev <[email protected]>
Date: Tue, 21 Apr 2026 15:46:04 +0300
Subject: [PATCH v1] Forbid FOR PORTION OF on views with INSTEAD OF triggers

Priviously an attempt to use these features together caused a crash.
Oversight of commit 8e72d914c528.

Author: Aleksander Alekseev <[email protected]>
Reviewed-by: TODO FIXME
Discussion: TODO FIXME
---
 src/backend/parser/analyze.c                  | 11 +++++++++
 src/test/regress/expected/updatable_views.out | 24 +++++++++++++++++++
 src/test/regress/sql/updatable_views.sql      | 24 +++++++++++++++++++
 3 files changed, 59 insertions(+)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index cb4e5019c2f..4fc0ae7199e 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1333,6 +1333,17 @@ transformForPortionOfClause(ParseState *pstate,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("foreign tables don't support FOR PORTION OF")));
 
+	/* We don't support FOR PORTION OF on views with INSTEAD OF triggers. */
+	if (targetrel->rd_rel->relkind == RELKIND_VIEW &&
+		targetrel->rd_rel->relhastriggers &&
+		targetrel->trigdesc != NULL &&
+		(isUpdate ? targetrel->trigdesc->trig_update_instead_row
+				  : targetrel->trigdesc->trig_delete_instead_row))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("views with INSTEAD OF triggers do not support FOR PORTION OF"),
+				 parser_errposition(pstate, forPortionOf->location)));
+
 	result = makeNode(ForPortionOfExpr);
 
 	/* Look up the FOR PORTION OF name requested. */
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8852160718f..4701938bfe2 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -4151,3 +4151,27 @@ select * from base_tab order by a;
 
 drop view base_tab_view;
 drop table base_tab;
+-- FOR PORTION OF is not supported on views with INSTEAD OF triggers
+create view uv_fpo_instead_view as select id, valid_at, b from uv_fpo_tab;
+create function uv_fpo_instead_trig() returns trigger language plpgsql as
+$$ begin return null; end $$;
+create trigger uv_fpo_instead_upd_trig
+  instead of update on uv_fpo_instead_view
+  for each row execute function uv_fpo_instead_trig();
+create trigger uv_fpo_instead_del_trig
+  instead of delete on uv_fpo_instead_view
+  for each row execute function uv_fpo_instead_trig();
+update uv_fpo_instead_view
+  for portion of valid_at from '2015-01-01' to '2020-01-01'
+  set b = 99 where id = '[1,1]'; -- error
+ERROR:  views with INSTEAD OF triggers do not support FOR PORTION OF
+LINE 2:   for portion of valid_at from '2015-01-01' to '2020-01-01'
+                         ^
+delete from uv_fpo_instead_view
+  for portion of valid_at from '2017-01-01' to '2022-01-01'
+  where id = '[1,1]'; -- error
+ERROR:  views with INSTEAD OF triggers do not support FOR PORTION OF
+LINE 2:   for portion of valid_at from '2017-01-01' to '2022-01-01'
+                         ^
+drop view uv_fpo_instead_view;
+drop function uv_fpo_instead_trig();
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index f7646999bd4..30c2db7ad7d 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -2137,3 +2137,27 @@ values (1, 2, default, 5, 4, default, 3), (10, 11, 'C value', 14, 13, 100, 12);
 select * from base_tab order by a;
 drop view base_tab_view;
 drop table base_tab;
+
+-- FOR PORTION OF is not supported on views with INSTEAD OF triggers
+create view uv_fpo_instead_view as select id, valid_at, b from uv_fpo_tab;
+
+create function uv_fpo_instead_trig() returns trigger language plpgsql as
+$$ begin return null; end $$;
+
+create trigger uv_fpo_instead_upd_trig
+  instead of update on uv_fpo_instead_view
+  for each row execute function uv_fpo_instead_trig();
+create trigger uv_fpo_instead_del_trig
+  instead of delete on uv_fpo_instead_view
+  for each row execute function uv_fpo_instead_trig();
+
+update uv_fpo_instead_view
+  for portion of valid_at from '2015-01-01' to '2020-01-01'
+  set b = 99 where id = '[1,1]'; -- error
+
+delete from uv_fpo_instead_view
+  for portion of valid_at from '2017-01-01' to '2022-01-01'
+  where id = '[1,1]'; -- error
+
+drop view uv_fpo_instead_view;
+drop function uv_fpo_instead_trig();
-- 
2.43.0

Reply via email to