public inbox for [email protected]  
help / color / mirror / Atom feed
From: Ayush Tiwari <[email protected]>
To: [email protected]
To: [email protected]
Subject: Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
Date: Sat, 25 Apr 2026 15:45:31 +0530
Message-ID: <CAJTYsWUq5e1D6fFGphCj+Xqx1LuWu1QhgCfOckNRk945vmK6PQ@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <[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



reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected]
  Subject: Re: BUG #19466: Server crash (SIGSEGV) when FETCH after ALTER TYPE during open cursor
  In-Reply-To: <CAJTYsWUq5e1D6fFGphCj+Xqx1LuWu1QhgCfOckNRk945vmK6PQ@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

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