public inbox for [email protected]  
help / color / mirror / Atom feed
BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
5+ messages / 3 participants
[nested] [flat]

* BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
@ 2026-04-25 07:27 PG Bug reporting form <[email protected]>
  2026-04-25 10:15 ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
  0 siblings, 1 reply; 5+ messages in thread

From: PG Bug reporting form @ 2026-04-25 07:27 UTC (permalink / raw)
  To: [email protected]; +Cc: [email protected]

The following bug has been logged on the website:

Bug reference:      19466
Logged by:          HaoGang Mao
Email address:      [email protected]
PostgreSQL version: 18.3
Operating system:   Linux
Description:        

PostgreSQL version: 18.3
OS: Linux (Docker)

Summary:
PostgreSQL crashes with SIGSEGV when a cursor is open over a composite
type and the type is modified via ALTER TYPE during the same transaction,
followed by a second FETCH.

Reproduction steps (minimal):
  CREATE TYPE foo AS (a INT, b INT);
  BEGIN;
  DECLARE c CURSOR FOR
    SELECT (i, power(2, 30))::foo
    FROM generate_series(1,10) i;
  FETCH c;
  ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
  FETCH c;
  COMMIT;

Expected: Error message (type modified during active cursor)
Actual:   Server process terminated with signal 11 (Segmentation fault)

Server log:
  client backend (PID 85) was terminated by signal 11: Segmentation fault
  Failed process was running: [above SQL]

Hypothesis:
The cursor holds a reference to the tuple descriptor for type "foo".
After ALTER TYPE modifies the type, the descriptor may be invalidated
while the cursor still holds a dangling pointer to it. The second FETCH
dereferences this invalid pointer.








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

* Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
  2026-04-25 07:27 BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor PG Bug reporting form <[email protected]>
@ 2026-04-25 10:15 ` Ayush Tiwari <[email protected]>
  2026-04-27 05:37   ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
  0 siblings, 1 reply; 5+ messages in thread

From: Ayush Tiwari @ 2026-04-25 10:15 UTC (permalink / raw)
  To: [email protected]; [email protected]

Hi,

On Sat, 25 Apr 2026 at 14:34, PG Bug reporting form <[email protected]>
wrote:

> The following bug has been logged on the website:
>
> Bug reference:      19466
> Logged by:          HaoGang Mao
> Email address:      [email protected]
> PostgreSQL version: 18.3
> Operating system:   Linux
> Description:
>
> PostgreSQL version: 18.3
> OS: Linux (Docker)
>
> Summary:
> PostgreSQL crashes with SIGSEGV when a cursor is open over a composite
> type and the type is modified via ALTER TYPE during the same transaction,
> followed by a second FETCH.
>
> Reproduction steps (minimal):
>   CREATE TYPE foo AS (a INT, b INT);
>   BEGIN;
>   DECLARE c CURSOR FOR
>     SELECT (i, power(2, 30))::foo
>     FROM generate_series(1,10) i;
>   FETCH c;
>   ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
>   FETCH c;
>   COMMIT;
>
> Expected: Error message (type modified during active cursor)
> Actual:   Server process terminated with signal 11 (Segmentation fault)
>
> Server log:
>   client backend (PID 85) was terminated by signal 11: Segmentation fault
>   Failed process was running: [above SQL]
>
> Hypothesis:
> The cursor holds a reference to the tuple descriptor for type "foo".
> After ALTER TYPE modifies the type, the descriptor may be invalidated
> while the cursor still holds a dangling pointer to it. The second FETCH
> dereferences this invalid pointer.



I confirmed the crash on master and traced the root cause. EEOP_ROW was the
only rowtype-aware expression step that cached its TupleDesc at init
time without an ExprEvalRowtypeCache guard. When ALTER TYPE changes
an attribute's storage properties (e.g. int to text), the stale
descriptor leads to SIGSEGV.

Attached patch adds the same ExprEvalRowtypeCache check that
EEOP_FIELDSELECT, EEOP_FIELDSTORE_DEFORM, etc. already use. With
the fix the reproducer gets a clean error instead of crashing.

Regards,
Ayush


Attachments:

  [application/octet-stream] v1-0001-Detect-row-type-changes-in-EEOP_ROW-expressions.patch (5.1K, 3-v1-0001-Detect-row-type-changes-in-EEOP_ROW-expressions.patch)
  download | inline diff:
From 5311d44fed4871a0cb0bc2b23be027ea38eee142 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <[email protected]>
Date: Sat, 25 Apr 2026 09:49:51 +0000
Subject: [PATCH] Detect row type changes in EEOP_ROW expressions

EEOP_ROW cached the target composite type's TupleDesc at executor
startup and never re-checked it.  If ALTER TYPE modified the type
while a cursor was still open, the next FETCH would use the stale
descriptor, which could crash the backend with SIGSEGV when attribute
storage properties changed (e.g. int -> text).

Other rowtype-aware steps (EEOP_FIELDSELECT, EEOP_FIELDSTORE_DEFORM,
EEOP_NULLTEST_ROW*, EEOP_CONVERT_ROWTYPE) already guard against this
via ExprEvalRowtypeCache.  Add the same check to EEOP_ROW: stash the
TypeCacheEntry pointer and tupDesc_identifier at init time, and
compare at runtime in ExecEvalRow(), raising an error if the type has
changed.

Reported-by: HaoGang Mao
---
 src/backend/executor/execExpr.c        | 15 ++++++++++++++-
 src/backend/executor/execExprInterp.c  | 11 +++++++++++
 src/include/executor/execExpr.h        |  1 +
 src/test/regress/expected/rowtypes.out | 17 +++++++++++++++++
 src/test/regress/sql/rowtypes.sql      | 12 ++++++++++++
 5 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 77229141b38..8b83e5d71ea 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -2011,11 +2011,24 @@ ExecInitExprRec(Expr *node, ExprState *state,
 					ExecTypeSetColNames(tupdesc, rowexpr->colnames);
 					/* Bless the tupdesc so it can be looked up later */
 					BlessTupleDesc(tupdesc);
+					scratch.d.row.rowcache.cacheptr = NULL;
+					scratch.d.row.rowcache.tupdesc_id = 0;
 				}
 				else
 				{
+					TypeCacheEntry *typentry;
+
 					/* it's been cast to a named type, use that */
-					tupdesc = lookup_rowtype_tupdesc_copy(rowexpr->row_typeid, -1);
+					typentry = lookup_type_cache(rowexpr->row_typeid,
+											   TYPECACHE_TUPDESC);
+					if (typentry->tupDesc == NULL)
+						ereport(ERROR,
+								(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+								 errmsg("type %s is not composite",
+										format_type_be(rowexpr->row_typeid))));
+					tupdesc = CreateTupleDescCopyConstr(typentry->tupDesc);
+					scratch.d.row.rowcache.cacheptr = typentry;
+					scratch.d.row.rowcache.tupdesc_id = typentry->tupDesc_identifier;
 				}
 
 				/*
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 0634af964a9..0ec06e87278 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -3667,6 +3667,17 @@ ExecEvalRow(ExprState *state, ExprEvalStep *op)
 {
 	HeapTuple	tuple;
 
+	if (op->d.row.rowcache.tupdesc_id != 0)
+	{
+		TypeCacheEntry *typentry = (TypeCacheEntry *) op->d.row.rowcache.cacheptr;
+
+		if (typentry->tupDesc_identifier != op->d.row.rowcache.tupdesc_id)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("row type %s has changed",
+							format_type_be(op->d.row.tupdesc->tdtypeid))));
+	}
+
 	/* build tuple from evaluated field values */
 	tuple = heap_form_tuple(op->d.row.tupdesc,
 							op->d.row.elemvalues,
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c61b3d624d5..532e01b7b6c 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -499,6 +499,7 @@ typedef struct ExprEvalStep
 		struct
 		{
 			TupleDesc	tupdesc;	/* descriptor for result tuples */
+			ExprEvalRowtypeCache rowcache;
 			/* workspace for the values constituting the row: */
 			Datum	   *elemvalues;
 			bool	   *elemnulls;
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 956bc2d02fc..7ede45b320a 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1408,3 +1408,20 @@ ERROR:  column "oid" not found in data type compositetable
 LINE 1: SELECT (NULL::compositetable).oid;
                 ^
 DROP TABLE compositetable;
+-- A named ROW() result must not survive ALTER TYPE with the old layout.
+CREATE TYPE cursor_rowtype AS (a int, b int);
+BEGIN;
+DECLARE c CURSOR FOR
+  SELECT (i, power(2, 30))::cursor_rowtype
+  FROM generate_series(1, 2) i;
+FETCH c;
+      row       
+----------------
+ (1,1073741824)
+(1 row)
+
+ALTER TYPE cursor_rowtype ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ERROR:  row type cursor_rowtype has changed
+ROLLBACK;
+DROP TYPE cursor_rowtype;
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index 174b062144a..ec64f968be8 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -562,3 +562,15 @@ SELECT (NULL::compositetable).a;
 SELECT (NULL::compositetable).oid;
 
 DROP TABLE compositetable;
+
+-- A named ROW() result must not survive ALTER TYPE with the old layout.
+CREATE TYPE cursor_rowtype AS (a int, b int);
+BEGIN;
+DECLARE c CURSOR FOR
+  SELECT (i, power(2, 30))::cursor_rowtype
+  FROM generate_series(1, 2) i;
+FETCH c;
+ALTER TYPE cursor_rowtype ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ROLLBACK;
+DROP TYPE cursor_rowtype;
-- 
2.43.0



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

* Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
  2026-04-25 07:27 BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor PG Bug reporting form <[email protected]>
  2026-04-25 10:15 ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
@ 2026-04-27 05:37   ` Ayush Tiwari <[email protected]>
  2026-04-27 12:34     ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor jian he <[email protected]>
  0 siblings, 1 reply; 5+ messages in thread

From: Ayush Tiwari @ 2026-04-27 05:37 UTC (permalink / raw)
  To: [email protected]; [email protected]; +Cc: David Rowley <[email protected]>

Hi,

On Sat, 25 Apr 2026 at 15:45, Ayush Tiwari <[email protected]>
wrote:

> Hi,
>
> On Sat, 25 Apr 2026 at 14:34, PG Bug reporting form <
> [email protected]> wrote:
>
>> The following bug has been logged on the website:
>>
>> Summary:
>> PostgreSQL crashes with SIGSEGV when a cursor is open over a composite
>> type and the type is modified via ALTER TYPE during the same transaction,
>> followed by a second FETCH.
>>
>> Reproduction steps (minimal):
>>   CREATE TYPE foo AS (a INT, b INT);
>>   BEGIN;
>>   DECLARE c CURSOR FOR
>>     SELECT (i, power(2, 30))::foo
>>     FROM generate_series(1,10) i;
>>   FETCH c;
>>   ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
>>   FETCH c;
>>   COMMIT;
>>
>> Expected: Error message (type modified during active cursor)
>> Actual:   Server process terminated with signal 11 (Segmentation fault)
>>
>> Server log:
>>   client backend (PID 85) was terminated by signal 11: Segmentation fault
>>   Failed process was running: [above SQL]
>>
>
>
> I confirmed the crash on master and traced the root cause. EEOP_ROW was the
> only rowtype-aware expression step that cached its TupleDesc at init
> time without an ExprEvalRowtypeCache guard. When ALTER TYPE changes
> an attribute's storage properties (e.g. int to text), the stale
> descriptor leads to SIGSEGV.
>
>
I looked through nearby rowtype-producing paths and found a few more cases
with the same general shape, so I split the proposed fix into two patches:

  0001: Detect row type changes in EEOP_ROW expressions

    EEOP_ROW was caching the target composite type's TupleDesc at executor
    startup and never re-checking it.  The patch stores the TypeCacheEntry
    pointer and tupDesc_identifier for named composite row results, then
    compares the identifier in ExecEvalRow() before forming the composite
    Datum.  If the type changed, the cursor now reports:

      ERROR:  row type <type-name> has changed

    instead of producing a malformed Datum that can later crash the backend.

  0002: Detect row type changes in additional composite result paths

    While checking for similar stale TupleDesc reuse, I found the same kind
    of guard is also needed for:

      - whole-row Vars returning a named composite type
      - SQL-language functions returning a whole named composite result
      - targetlist SRFs returning named composite results

    The second patch adds the same typcache TupleDesc identity check in
    those paths and adds regression coverage for cursor/ALTER TYPE/FETCH
    cases that previously crashed or could reuse stale layout information.

With these patches, the original repro and the additional repro cases fail
cleanly with "row type ... has changed" errors, and the backend remains
running.

Thoughts?

Regards,
Ayush


Attachments:

  [application/octet-stream] v1-0001-Detect-row-type-changes-in-EEOP_ROW-expressions 2.patch (5.1K, 3-v1-0001-Detect-row-type-changes-in-EEOP_ROW-expressions%202.patch)
  download | inline diff:
From 662f676c007eb6ae949deea2223b00a8b8a28e0b Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <[email protected]>
Date: Mon, 27 Apr 2026 05:10:39 +0000
Subject: [PATCH v1 1/2] Detect row type changes in EEOP_ROW expressions

EEOP_ROW cached the target composite type's TupleDesc at executor
startup and never re-checked it.  If ALTER TYPE modified the type
while a cursor was still open, the next FETCH would use the stale
descriptor, which could crash the backend with SIGSEGV when attribute
storage properties changed (e.g. int -> text).

Other rowtype-aware steps (EEOP_FIELDSELECT, EEOP_FIELDSTORE_DEFORM,
EEOP_NULLTEST_ROW*, EEOP_CONVERT_ROWTYPE) already guard against this
via ExprEvalRowtypeCache.  Add the same check to EEOP_ROW: stash the
TypeCacheEntry pointer and tupDesc_identifier at init time, and
compare at runtime in ExecEvalRow(), raising an error if the type has
changed.
---
 src/backend/executor/execExpr.c        | 15 ++++++++++++++-
 src/backend/executor/execExprInterp.c  | 11 +++++++++++
 src/include/executor/execExpr.h        |  1 +
 src/test/regress/expected/rowtypes.out | 17 +++++++++++++++++
 src/test/regress/sql/rowtypes.sql      | 12 ++++++++++++
 5 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 77229141b38..8b83e5d71ea 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -2011,11 +2011,24 @@ ExecInitExprRec(Expr *node, ExprState *state,
 					ExecTypeSetColNames(tupdesc, rowexpr->colnames);
 					/* Bless the tupdesc so it can be looked up later */
 					BlessTupleDesc(tupdesc);
+					scratch.d.row.rowcache.cacheptr = NULL;
+					scratch.d.row.rowcache.tupdesc_id = 0;
 				}
 				else
 				{
+					TypeCacheEntry *typentry;
+
 					/* it's been cast to a named type, use that */
-					tupdesc = lookup_rowtype_tupdesc_copy(rowexpr->row_typeid, -1);
+					typentry = lookup_type_cache(rowexpr->row_typeid,
+											   TYPECACHE_TUPDESC);
+					if (typentry->tupDesc == NULL)
+						ereport(ERROR,
+								(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+								 errmsg("type %s is not composite",
+										format_type_be(rowexpr->row_typeid))));
+					tupdesc = CreateTupleDescCopyConstr(typentry->tupDesc);
+					scratch.d.row.rowcache.cacheptr = typentry;
+					scratch.d.row.rowcache.tupdesc_id = typentry->tupDesc_identifier;
 				}
 
 				/*
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 0634af964a9..0ec06e87278 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -3667,6 +3667,17 @@ ExecEvalRow(ExprState *state, ExprEvalStep *op)
 {
 	HeapTuple	tuple;
 
+	if (op->d.row.rowcache.tupdesc_id != 0)
+	{
+		TypeCacheEntry *typentry = (TypeCacheEntry *) op->d.row.rowcache.cacheptr;
+
+		if (typentry->tupDesc_identifier != op->d.row.rowcache.tupdesc_id)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("row type %s has changed",
+							format_type_be(op->d.row.tupdesc->tdtypeid))));
+	}
+
 	/* build tuple from evaluated field values */
 	tuple = heap_form_tuple(op->d.row.tupdesc,
 							op->d.row.elemvalues,
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c61b3d624d5..532e01b7b6c 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -499,6 +499,7 @@ typedef struct ExprEvalStep
 		struct
 		{
 			TupleDesc	tupdesc;	/* descriptor for result tuples */
+			ExprEvalRowtypeCache rowcache;
 			/* workspace for the values constituting the row: */
 			Datum	   *elemvalues;
 			bool	   *elemnulls;
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 956bc2d02fc..7ede45b320a 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1408,3 +1408,20 @@ ERROR:  column "oid" not found in data type compositetable
 LINE 1: SELECT (NULL::compositetable).oid;
                 ^
 DROP TABLE compositetable;
+-- A named ROW() result must not survive ALTER TYPE with the old layout.
+CREATE TYPE cursor_rowtype AS (a int, b int);
+BEGIN;
+DECLARE c CURSOR FOR
+  SELECT (i, power(2, 30))::cursor_rowtype
+  FROM generate_series(1, 2) i;
+FETCH c;
+      row       
+----------------
+ (1,1073741824)
+(1 row)
+
+ALTER TYPE cursor_rowtype ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ERROR:  row type cursor_rowtype has changed
+ROLLBACK;
+DROP TYPE cursor_rowtype;
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index 174b062144a..ec64f968be8 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -562,3 +562,15 @@ SELECT (NULL::compositetable).a;
 SELECT (NULL::compositetable).oid;
 
 DROP TABLE compositetable;
+
+-- A named ROW() result must not survive ALTER TYPE with the old layout.
+CREATE TYPE cursor_rowtype AS (a int, b int);
+BEGIN;
+DECLARE c CURSOR FOR
+  SELECT (i, power(2, 30))::cursor_rowtype
+  FROM generate_series(1, 2) i;
+FETCH c;
+ALTER TYPE cursor_rowtype ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ROLLBACK;
+DROP TYPE cursor_rowtype;
-- 
2.43.0



  [application/octet-stream] v1-0002-Detect-row-type-changes-in-additional-rowtype-result-paths 1.patch (16.0K, 4-v1-0002-Detect-row-type-changes-in-additional-rowtype-result-paths%201.patch)
  download | inline diff:
From 050a1e405a95d00fc6bf17cf9bd9079b9131fdb0 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <[email protected]>
Date: Mon, 27 Apr 2026 05:20:16 +0000
Subject: [PATCH v1 2/2] Detect row type changes in additional composite result
 paths

EEOP_ROW is not the only long-lived executor path that can reuse a
named composite TupleDesc after ALTER TYPE changes the row layout.  A
whole-row Var, a SQL function returning a whole composite result, and a
targetlist SRF returning a composite result can all cache or copy a
descriptor and later form or return a composite Datum using stale layout
information.

Track the typcache TupleDesc identity in those paths and raise the same
"row type has changed" error if the named row type changes before the
cached descriptor is reused.  Add regression coverage for the additional
cursor/DDL/FETCH-again cases that previously crashed.
---
 src/backend/executor/execExpr.c        |  8 ++++
 src/backend/executor/execExprInterp.c  | 36 +++++++++++++++++
 src/backend/executor/execSRF.c         | 49 ++++++++++++++++++++++
 src/backend/executor/functions.c       | 46 +++++++++++++++++++++
 src/include/executor/execExpr.h        |  1 +
 src/include/nodes/execnodes.h          |  2 +
 src/test/regress/expected/rowtypes.out | 56 ++++++++++++++++++++++++++
 src/test/regress/sql/rowtypes.sql      | 39 ++++++++++++++++++
 8 files changed, 237 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 8b83e5d71ea..88e271079d6 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -3212,6 +3212,14 @@ ExecInitWholeRowVar(ExprEvalStep *scratch, Var *variable, ExprState *state)
 	scratch->d.wholerow.first = true;
 	scratch->d.wholerow.slow = false;
 	scratch->d.wholerow.tupdesc = NULL; /* filled at runtime */
+	if (variable->vartype != RECORDOID)
+	{
+		scratch->d.wholerow.rowcache = palloc_object(ExprEvalRowtypeCache);
+		scratch->d.wholerow.rowcache->cacheptr = NULL;
+		scratch->d.wholerow.rowcache->tupdesc_id = 0;
+	}
+	else
+		scratch->d.wholerow.rowcache = NULL;
 	scratch->d.wholerow.junkFilter = NULL;
 
 	/* update ExprState flags if Var refers to OLD/NEW */
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 0ec06e87278..c448e94ecd1 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -2546,6 +2546,26 @@ get_cached_rowtype(Oid type_id, int32 typmod,
 	}
 }
 
+static TypeCacheEntry *
+lookup_named_rowtype_tcache(Oid type_id)
+{
+	TypeCacheEntry *typentry;
+
+	typentry = lookup_type_cache(type_id,
+							  TYPECACHE_TUPDESC |
+							  TYPECACHE_DOMAIN_BASE_INFO);
+	if (typentry->typtype == TYPTYPE_DOMAIN)
+		typentry = lookup_type_cache(typentry->domainBaseType,
+								   TYPECACHE_TUPDESC);
+	if (typentry->tupDesc == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("type %s is not composite",
+						format_type_be(type_id))));
+
+	return typentry;
+}
+
 
 /*
  * Fast-path functions, for very simple expressions
@@ -5495,6 +5515,7 @@ ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 		 */
 		if (variable->vartype != RECORDOID)
 		{
+			TypeCacheEntry *typentry;
 			TupleDesc	var_tupdesc;
 			TupleDesc	slot_tupdesc;
 
@@ -5513,6 +5534,10 @@ ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 			 * If vartype is a domain over composite, just look through that
 			 * to the base composite type.
 			 */
+			typentry = lookup_named_rowtype_tcache(variable->vartype);
+			op->d.wholerow.rowcache->cacheptr = typentry;
+			op->d.wholerow.rowcache->tupdesc_id = typentry->tupDesc_identifier;
+
 			var_tupdesc = lookup_rowtype_tupdesc_domain(variable->vartype,
 														-1, false);
 
@@ -5609,6 +5634,17 @@ ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 
 		op->d.wholerow.first = false;
 	}
+	else if (variable->vartype != RECORDOID)
+	{
+		TypeCacheEntry *typentry;
+
+		typentry = (TypeCacheEntry *) op->d.wholerow.rowcache->cacheptr;
+		if (typentry->tupDesc_identifier != op->d.wholerow.rowcache->tupdesc_id)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("row type %s has changed",
+							format_type_be(op->d.wholerow.tupdesc->tdtypeid))));
+	}
 
 	/*
 	 * Make sure all columns of the slot are accessible in the slot's
diff --git a/src/backend/executor/execSRF.c b/src/backend/executor/execSRF.c
index 8aedcc6a459..50999b88769 100644
--- a/src/backend/executor/execSRF.c
+++ b/src/backend/executor/execSRF.c
@@ -21,6 +21,7 @@
 #include "access/htup_details.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -45,6 +46,8 @@ static void ExecPrepareTuplestoreResult(SetExprState *sexpr,
 										ExprContext *econtext,
 										Tuplestorestate *resultStore,
 										TupleDesc resultDesc);
+static TypeCacheEntry *lookup_srf_result_tcache(Oid funcrettype);
+static void check_srf_result_rowtype(SetExprState *sexpr);
 static void tupledesc_match(TupleDesc dst_tupdesc, TupleDesc src_tupdesc);
 
 
@@ -543,6 +546,7 @@ restart:
 			{
 				/* We must return the whole tuple as a Datum. */
 				*isNull = false;
+				check_srf_result_rowtype(fcache);
 				return ExecFetchSlotHeapTupleDatum(fcache->funcResultSlot);
 			}
 			else
@@ -643,6 +647,9 @@ restart:
 	{
 		if (*isDone != ExprEndResult)
 		{
+			if (fcache->funcReturnsTuple && !*isNull)
+				check_srf_result_rowtype(fcache);
+
 			/*
 			 * Save the current argument values to re-use on the next call.
 			 */
@@ -669,6 +676,9 @@ restart:
 					 errmsg("table-function protocol for materialize mode was not followed")));
 		if (rsinfo.setResult != NULL)
 		{
+			if (fcache->funcReturnsTuple)
+				check_srf_result_rowtype(fcache);
+
 			/* prepare to return values from the tuplestore */
 			ExecPrepareTuplestoreResult(fcache, econtext,
 										rsinfo.setResult,
@@ -767,6 +777,10 @@ init_sexpr(Oid foid, Oid input_collation, Expr *node,
 			/* Must copy it out of typcache for safety */
 			sexpr->funcResultDesc = CreateTupleDescCopy(tupdesc);
 			sexpr->funcReturnsTuple = true;
+			sexpr->funcResultTypentry = lookup_srf_result_tcache(funcrettype);
+			if (sexpr->funcResultTypentry != NULL)
+				sexpr->funcResultDescId =
+					((TypeCacheEntry *) sexpr->funcResultTypentry)->tupDesc_identifier;
 		}
 		else if (functypclass == TYPEFUNC_SCALAR)
 		{
@@ -870,6 +884,8 @@ ExecPrepareTuplestoreResult(SetExprState *sexpr,
 							TupleDesc resultDesc)
 {
 	sexpr->funcResultStore = resultStore;
+	if (sexpr->funcReturnsTuple)
+		check_srf_result_rowtype(sexpr);
 
 	if (sexpr->funcResultSlot == NULL)
 	{
@@ -932,6 +948,39 @@ ExecPrepareTuplestoreResult(SetExprState *sexpr,
 	}
 }
 
+static TypeCacheEntry *
+lookup_srf_result_tcache(Oid funcrettype)
+{
+	TypeCacheEntry *typentry;
+
+	if (funcrettype == RECORDOID)
+		return NULL;
+
+	typentry = lookup_type_cache(funcrettype,
+							  TYPECACHE_TUPDESC |
+							  TYPECACHE_DOMAIN_BASE_INFO);
+	if (typentry->typtype == TYPTYPE_DOMAIN)
+		typentry = lookup_type_cache(typentry->domainBaseType,
+								   TYPECACHE_TUPDESC);
+	if (typentry->tupDesc == NULL)
+		return NULL;
+
+	return typentry;
+}
+
+static void
+check_srf_result_rowtype(SetExprState *sexpr)
+{
+	TypeCacheEntry *typentry = (TypeCacheEntry *) sexpr->funcResultTypentry;
+
+	if (typentry != NULL &&
+		typentry->tupDesc_identifier != sexpr->funcResultDescId)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("row type %s has changed",
+						format_type_be(sexpr->funcResultDesc->tdtypeid))));
+}
+
 /*
  * Check that function result tuple type (src_tupdesc) matches or can
  * be considered to match what the query expects (dst_tupdesc). If
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 88109348817..89a8b760293 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -38,6 +38,7 @@
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 #include "utils/tuplestore.h"
+#include "utils/typcache.h"
 
 
 /*
@@ -129,6 +130,8 @@ typedef struct SQLFunctionHashEntry
 	char		prokind;		/* prokind from pg_proc row */
 
 	TupleDesc	rettupdesc;		/* result tuple descriptor */
+	TypeCacheEntry *rettupdesc_typentry;
+	uint64		rettupdesc_id;
 
 	List	   *source_list;	/* RawStmts or Queries read from pg_proc */
 	int			num_queries;	/* original length of source_list */
@@ -203,6 +206,8 @@ static Node *sql_fn_resolve_param_name(SQLFunctionParseInfoPtr pinfo,
 									   const char *paramname, int location);
 static SQLFunctionCache *init_sql_fcache(FunctionCallInfo fcinfo,
 										 bool lazyEvalOK);
+static TypeCacheEntry *lookup_sql_fn_retval_tcache(Oid rettype);
+static void check_sql_fn_retval_rowtype(SQLFunctionCachePtr fcache);
 static bool init_execution_state(SQLFunctionCachePtr fcache);
 static void prepare_next_query(SQLFunctionHashEntry *func);
 static void sql_compile_callback(FunctionCallInfo fcinfo,
@@ -1102,6 +1107,10 @@ sql_compile_callback(FunctionCallInfo fcinfo,
 		MemoryContextSwitchTo(hcontext);
 		func->rettupdesc = CreateTupleDescCopy(rettupdesc);
 		MemoryContextSwitchTo(oldcontext);
+
+		func->rettupdesc_typentry = lookup_sql_fn_retval_tcache(rettype);
+		if (func->rettupdesc_typentry != NULL)
+			func->rettupdesc_id = func->rettupdesc_typentry->tupDesc_identifier;
 	}
 
 	/* Fetch the typlen and byval info for the result type */
@@ -1550,6 +1559,7 @@ postquel_get_single_result(TupleTableSlot *slot,
 	{
 		/* We must return the whole tuple as a Datum. */
 		fcinfo->isnull = false;
+		check_sql_fn_retval_rowtype(fcache);
 		value = ExecFetchSlotHeapTupleDatum(slot);
 	}
 	else
@@ -1828,7 +1838,10 @@ fmgr_sql(PG_FUNCTION_ARGS)
 			fcache->tstore = NULL;
 			/* must copy desc because execSRF.c will free it */
 			if (fcache->junkFilter)
+			{
+				check_sql_fn_retval_rowtype(fcache);
 				rsi->setDesc = CreateTupleDescCopy(fcache->junkFilter->jf_cleanTupType);
+			}
 
 			fcinfo->isnull = true;
 			result = (Datum) 0;
@@ -1888,6 +1901,39 @@ fmgr_sql(PG_FUNCTION_ARGS)
 	return result;
 }
 
+static TypeCacheEntry *
+lookup_sql_fn_retval_tcache(Oid rettype)
+{
+	TypeCacheEntry *typentry;
+
+	if (rettype == RECORDOID)
+		return NULL;
+
+	typentry = lookup_type_cache(rettype,
+							  TYPECACHE_TUPDESC |
+							  TYPECACHE_DOMAIN_BASE_INFO);
+	if (typentry->typtype == TYPTYPE_DOMAIN)
+		typentry = lookup_type_cache(typentry->domainBaseType,
+								   TYPECACHE_TUPDESC);
+	if (typentry->tupDesc == NULL)
+		return NULL;
+
+	return typentry;
+}
+
+static void
+check_sql_fn_retval_rowtype(SQLFunctionCachePtr fcache)
+{
+	TypeCacheEntry *typentry = fcache->func->rettupdesc_typentry;
+
+	if (typentry != NULL &&
+		typentry->tupDesc_identifier != fcache->func->rettupdesc_id)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("row type %s has changed",
+						format_type_be(fcache->func->rettupdesc->tdtypeid))));
+}
+
 
 /*
  * error context callback to let us supply a traceback during compile
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 532e01b7b6c..e5be3c185bf 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -348,6 +348,7 @@ typedef struct ExprEvalStep
 			bool		first;	/* first time through, need to initialize? */
 			bool		slow;	/* need runtime check for nulls? */
 			TupleDesc	tupdesc;	/* descriptor for resulting tuples */
+			ExprEvalRowtypeCache *rowcache; /* cached descriptor identity */
 			JunkFilter *junkFilter; /* JunkFilter to remove resjunk cols */
 		}			wholerow;
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 13359180d25..8c5c15af9d1 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1006,6 +1006,8 @@ typedef struct SetExprState
 	 */
 	TupleDesc	funcResultDesc;
 	bool		funcReturnsTuple;	/* valid when funcResultDesc isn't NULL */
+	void	   *funcResultTypentry;	/* cached TypeCacheEntry for result rowtype */
+	uint64		funcResultDescId;	/* last-seen tupdesc identifier, or 0 */
 
 	/*
 	 * Remember whether the function is declared to return a set.  This is set
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 7ede45b320a..f468fd56d37 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1425,3 +1425,59 @@ FETCH c;
 ERROR:  row type cursor_rowtype has changed
 ROLLBACK;
 DROP TYPE cursor_rowtype;
+-- A whole-row Var over a named row type must detect ALTER TYPE too.
+CREATE TYPE cursor_wholerow_type AS (a int, b int);
+CREATE FUNCTION cursor_wholerow_func() RETURNS SETOF cursor_wholerow_type
+LANGUAGE sql AS $$ SELECT i, power(2, 30)::int FROM generate_series(1, 2) i $$;
+BEGIN;
+DECLARE c CURSOR FOR SELECT x FROM cursor_wholerow_func() AS x;
+FETCH c;
+       x        
+----------------
+ (1,1073741824)
+(1 row)
+
+ALTER TYPE cursor_wholerow_type ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ERROR:  row type cursor_wholerow_type has changed
+ROLLBACK;
+DROP FUNCTION cursor_wholerow_func();
+DROP TYPE cursor_wholerow_type;
+-- A SQL function returning a whole named row must not use its old tupdesc.
+CREATE TYPE cursor_sqlfunc_type AS (a int, b int);
+CREATE FUNCTION cursor_sqlfunc(i int) RETURNS cursor_sqlfunc_type
+LANGUAGE sql AS $$ SELECT i, power(2, 30)::int $$;
+BEGIN;
+DECLARE c CURSOR FOR SELECT cursor_sqlfunc(i) FROM generate_series(1, 2) i;
+FETCH c;
+ cursor_sqlfunc 
+----------------
+ (1,1073741824)
+(1 row)
+
+ALTER TYPE cursor_sqlfunc_type ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ERROR:  row type cursor_sqlfunc_type has changed
+CONTEXT:  SQL function "cursor_sqlfunc" statement 1
+ROLLBACK;
+DROP FUNCTION cursor_sqlfunc(int);
+DROP TYPE cursor_sqlfunc_type;
+-- A targetlist SRF returning a named row must not use its old tupdesc.
+CREATE TYPE cursor_srf_type AS (a int, b int);
+CREATE FUNCTION cursor_srf_func() RETURNS SETOF cursor_srf_type
+LANGUAGE sql AS $$ SELECT i, power(2, 30)::int FROM generate_series(1, 2) i $$;
+BEGIN;
+DECLARE c CURSOR FOR SELECT cursor_srf_func();
+FETCH c;
+ cursor_srf_func 
+-----------------
+ (1,1073741824)
+(1 row)
+
+ALTER TYPE cursor_srf_type ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ERROR:  row type cursor_srf_type has changed
+CONTEXT:  SQL function "cursor_srf_func" statement 1
+ROLLBACK;
+DROP FUNCTION cursor_srf_func();
+DROP TYPE cursor_srf_type;
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index ec64f968be8..8b4aee23aeb 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -574,3 +574,42 @@ ALTER TYPE cursor_rowtype ALTER ATTRIBUTE b TYPE text;
 FETCH c;
 ROLLBACK;
 DROP TYPE cursor_rowtype;
+
+-- A whole-row Var over a named row type must detect ALTER TYPE too.
+CREATE TYPE cursor_wholerow_type AS (a int, b int);
+CREATE FUNCTION cursor_wholerow_func() RETURNS SETOF cursor_wholerow_type
+LANGUAGE sql AS $$ SELECT i, power(2, 30)::int FROM generate_series(1, 2) i $$;
+BEGIN;
+DECLARE c CURSOR FOR SELECT x FROM cursor_wholerow_func() AS x;
+FETCH c;
+ALTER TYPE cursor_wholerow_type ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ROLLBACK;
+DROP FUNCTION cursor_wholerow_func();
+DROP TYPE cursor_wholerow_type;
+
+-- A SQL function returning a whole named row must not use its old tupdesc.
+CREATE TYPE cursor_sqlfunc_type AS (a int, b int);
+CREATE FUNCTION cursor_sqlfunc(i int) RETURNS cursor_sqlfunc_type
+LANGUAGE sql AS $$ SELECT i, power(2, 30)::int $$;
+BEGIN;
+DECLARE c CURSOR FOR SELECT cursor_sqlfunc(i) FROM generate_series(1, 2) i;
+FETCH c;
+ALTER TYPE cursor_sqlfunc_type ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ROLLBACK;
+DROP FUNCTION cursor_sqlfunc(int);
+DROP TYPE cursor_sqlfunc_type;
+
+-- A targetlist SRF returning a named row must not use its old tupdesc.
+CREATE TYPE cursor_srf_type AS (a int, b int);
+CREATE FUNCTION cursor_srf_func() RETURNS SETOF cursor_srf_type
+LANGUAGE sql AS $$ SELECT i, power(2, 30)::int FROM generate_series(1, 2) i $$;
+BEGIN;
+DECLARE c CURSOR FOR SELECT cursor_srf_func();
+FETCH c;
+ALTER TYPE cursor_srf_type ALTER ATTRIBUTE b TYPE text;
+FETCH c;
+ROLLBACK;
+DROP FUNCTION cursor_srf_func();
+DROP TYPE cursor_srf_type;
-- 
2.43.0



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

* Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
  2026-04-25 07:27 BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor PG Bug reporting form <[email protected]>
  2026-04-25 10:15 ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
  2026-04-27 05:37   ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
@ 2026-04-27 12:34     ` jian he <[email protected]>
  2026-04-27 12:41       ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
  0 siblings, 1 reply; 5+ messages in thread

From: jian he @ 2026-04-27 12:34 UTC (permalink / raw)
  To: Ayush Tiwari <[email protected]>; +Cc: [email protected]; [email protected]; David Rowley <[email protected]>

On Mon, Apr 27, 2026 at 1:37 PM Ayush Tiwari
<[email protected]> wrote:
>
> Thoughts?
>

This bug is the same as the reported in this[1] email message.
You may need to check that thread also.

[1]: https://www.postgresql.org/message-id/19382-4c2060ffee72759b%40postgresql.org






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

* Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
  2026-04-25 07:27 BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor PG Bug reporting form <[email protected]>
  2026-04-25 10:15 ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
  2026-04-27 05:37   ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor Ayush Tiwari <[email protected]>
  2026-04-27 12:34     ` Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor jian he <[email protected]>
@ 2026-04-27 12:41       ` Ayush Tiwari <[email protected]>
  0 siblings, 0 replies; 5+ messages in thread

From: Ayush Tiwari @ 2026-04-27 12:41 UTC (permalink / raw)
  To: jian he <[email protected]>; +Cc: [email protected]; [email protected]; David Rowley <[email protected]>

Hi Jian,

On Mon, 27 Apr 2026 at 18:04, jian he <[email protected]> wrote:

> On Mon, Apr 27, 2026 at 1:37 PM Ayush Tiwari
> <[email protected]> wrote:
> >
> > Thoughts?
> >
>
> This bug is the same as the reported in this[1] email message.
> You may need to check that thread also.
>
> [1]:
> https://www.postgresql.org/message-id/19382-4c2060ffee72759b%40postgresql.org


Thanks for the information! I'll go through that thread and
continue it there post testing that patch.

Regards,
Ayush


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


end of thread, other threads:[~2026-04-27 12:41 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-04-25 07:27 BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor PG Bug reporting form <[email protected]>
2026-04-25 10:15 ` Ayush Tiwari <[email protected]>
2026-04-27 05:37   ` Ayush Tiwari <[email protected]>
2026-04-27 12:34     ` jian he <[email protected]>
2026-04-27 12:41       ` Ayush Tiwari <[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