From 5c8867d4230beeb1a3d12cf45d699ea9ad027180 Mon Sep 17 00:00:00 2001
From: spoondla <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v4] Fix (bug #19382) server crash when ALTER TYPE is used
 mid-transaction in PL/pgSQL

When ALTER TYPE changes a composite type's column types within a
transaction, PL/pgSQL record variables that were populated before
the ALTER still hold data in the old format. Returning such records
causes a crash because the output functions expect data matching the
new type definition, not the old one.

The crash manifested as a segmentation fault in record_out() when it
attempted to interpret integer data as a text pointer, due to the
mismatch between the stored data and the current type definition.
---
 src/pl/plpgsql/src/pl_exec.c | 69 +++++++++++++++++++++++++++++++++++-
 1 file changed, 68 insertions(+), 1 deletion(-)

diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..6250c4a748b 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,7 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
 static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
 											  PLpgSQL_expr *expr);
 
+static void check_record_type_not_altered(PLpgSQL_rec *rec);
 
 /* ----------
  * plpgsql_exec_function	Called by the call handler for
@@ -3287,8 +3288,30 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
 				}
 				break;
 
-			case PLPGSQL_DTYPE_ROW:
 			case PLPGSQL_DTYPE_REC:
+				{
+					PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
+					int32		rettypmod;
+
+					/*
+					 * Check if the record's composite type was altered since
+					 * the record was populated. If so, raise an error to
+					 * prevent crashes when outputting the record.
+					 */
+					if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+						!ExpandedRecordIsEmpty(rec->erh))
+						check_record_type_not_altered(rec);
+
+					exec_eval_datum(estate,
+									retvar,
+									&estate->rettype,
+									&rettypmod,
+									&estate->retval,
+									&estate->retisnull);
+				}
+				break;
+
+			case PLPGSQL_DTYPE_ROW:
 				{
 					/* exec_eval_datum can handle these cases */
 					int32		rettypmod;
@@ -3434,6 +3457,14 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
 					TupleDesc	rec_tupdesc;
 					TupleConversionMap *tupmap;
 
+					/*
+					 * Check if the record's composite type was altered since
+					 * the record was populated. If so, raise an error to
+					 * prevent crashes when storing to the tuplestore.
+					 */
+					if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+						check_record_type_not_altered(rec);
+
 					/* If rec is null, try to convert it to a row of nulls */
 					if (rec->erh == NULL)
 						instantiate_empty_record_variable(estate, rec);
@@ -9216,3 +9247,39 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
 
 	return paramstr.data;
 }
+
+/*
+ * check_record_type_not_altered
+ *
+ * Check if a record's composite type has been altered since the record
+ * was populated. If so, raise an error to prevent crashes that would
+ * occur when outputting data that no longer matches the current type
+ * definition.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+	ExpandedRecordHeader *erh = rec->erh;
+	TypeCacheEntry *typentry;
+
+	/* Nothing to do for anonymous RECORD type */
+	if (rec->rectypeid == RECORDOID)
+		return;
+
+	/* Get current type definition from typcache */
+	typentry = lookup_type_cache(rec->rectypeid,
+								 TYPECACHE_TUPDESC |
+								 TYPECACHE_DOMAIN_BASE_INFO);
+	if (typentry->typtype == TYPTYPE_DOMAIN)
+		typentry = lookup_type_cache(typentry->domainBaseType,
+									 TYPECACHE_TUPDESC);
+
+	/* If type has changed since the record was populated, raise an error */
+	if (erh->er_tupdesc_id != typentry->tupDesc_identifier)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("cannot return record variable \"%s\" after its composite type was altered",
+						rec->refname),
+				 errdetail("ALTER TYPE changed the definition of type \"%s\" after the record was populated.",
+						   format_type_be(rec->rectypeid))));
+}
-- 
2.39.5 (Apple Git-154)

