From 2d83dde32a3a94b7298399c3f47c38f702cc96e7 Mon Sep 17 00:00:00 2001
From: spoondla <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v5] 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.

The fix snapshots tupDesc_identifier values for all composite types
reachable from a record variable's type (including nested composite
types) at assignment time. At RETURN/RETURN NEXT time, these
identifiers are compared against current values from the type cache.
If any have changed, an error is raised instead of risking a crash.
---
 .../plpgsql/src/expected/plpgsql_record.out   |  67 ++++++
 src/pl/plpgsql/src/pl_exec.c                  | 209 +++++++++++++++++-
 src/pl/plpgsql/src/plpgsql.h                  |   9 +
 src/pl/plpgsql/src/sql/plpgsql_record.sql     |  58 +++++
 4 files changed, 342 insertions(+), 1 deletion(-)

diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 511f9e03c85..ad21f8bbf3f 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,70 @@ table two_int8s_tab;
  (42,42)
 (1 row)
 
+-- Tests for bug #19382: server crash when ALTER TYPE is used mid-transaction
+-- in PL/pgSQL. Record variables populated before ALTER TYPE must not be
+-- returned, as the stored data no longer matches the current type definition.
+-- Case 1: Direct composite type change (INT -> TEXT)
+create type bug19382_foo as (a int, b int);
+create function bug19382_test_direct() returns record as $$
+declare r bug19382_foo := row(123, power(2, 30));
+begin
+    alter type bug19382_foo alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_direct();
+ERROR:  cannot return record variable "r" after composite type "bug19382_foo" was altered
+HINT:  Reassign the record variable after ALTER TYPE.
+CONTEXT:  PL/pgSQL function bug19382_test_direct() line 5 at RETURN
+drop function bug19382_test_direct();
+drop type bug19382_foo cascade;
+-- Case 2: Nested composite type change
+create type bug19382_inner as (x int, y int);
+create type bug19382_outer as (a int, b bug19382_inner);
+create function bug19382_test_nested() returns record as $$
+declare r bug19382_outer;
+begin
+    r := row(1, row(10, power(2, 30)::int4)::bug19382_inner)::bug19382_outer;
+    alter type bug19382_inner alter attribute y type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_nested();
+ERROR:  cannot return record variable "r" after composite type "bug19382_inner" was altered
+HINT:  Reassign the record variable after ALTER TYPE.
+CONTEXT:  PL/pgSQL function bug19382_test_nested() line 6 at RETURN
+drop function bug19382_test_nested();
+drop type bug19382_outer cascade;
+drop type bug19382_inner cascade;
+-- Case 3: OUT parameter
+create type bug19382_foo1 as (a int, b int);
+create function bug19382_test_out(out r1 bug19382_foo1) as $$
+begin
+    r1 := row(1, 2);
+    alter type bug19382_foo1 alter attribute b type text;
+    return;
+end;
+$$ language plpgsql;
+select bug19382_test_out();
+ERROR:  cannot return record variable "r1" after composite type "bug19382_foo1" was altered
+HINT:  Reassign the record variable after ALTER TYPE.
+CONTEXT:  PL/pgSQL function bug19382_test_out() line 5 at RETURN
+drop function bug19382_test_out();
+drop type bug19382_foo1 cascade;
+-- Case 4: No ALTER TYPE (baseline — must not error)
+create type bug19382_foo2 as (a int, b int);
+create function bug19382_test_baseline() returns bug19382_foo2 as $$
+declare r bug19382_foo2 := row(1, 2);
+begin
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_baseline();
+ bug19382_test_baseline 
+------------------------
+ (1,2)
+(1 row)
+
+drop function bug19382_test_baseline();
+drop type bug19382_foo2;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..be3529445f9 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,12 @@ 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);
+static void collect_composite_type_versions(Oid typid,
+											Oid **oids, uint64 **versions,
+											int *n, int *alloc);
+static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
+											PLpgSQL_rec *rec);
 
 /* ----------
  * plpgsql_exec_function	Called by the call handler for
@@ -3287,8 +3293,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 +3462,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);
@@ -7042,6 +7078,10 @@ exec_move_row(PLpgSQL_execstate *estate,
 				if (rec->erh)
 					DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
 				rec->erh = NULL;
+				/* Clear composite type snapshot */
+				rec->nCompTypes = 0;
+				rec->compTypeOids = NULL;
+				rec->compTypeVersions = NULL;
 			}
 			return;
 		}
@@ -8967,6 +9007,9 @@ assign_record_var(PLpgSQL_execstate *estate, PLpgSQL_rec *rec,
 
 	/* ... and install the new */
 	rec->erh = erh;
+
+	/* Snapshot composite type versions for ALTER TYPE detection */
+	snapshot_record_composite_types(estate, rec);
 }
 
 /*
@@ -9216,3 +9259,167 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
 
 	return paramstr.data;
 }
+
+/*
+ * check_record_type_not_altered
+ *
+ * Check if any composite type reachable from this record's 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.
+ *
+ * Uses the composite type version snapshot taken at record assignment time
+ * to detect changes in both the outermost type and any nested composite types.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+	int			i;
+
+	/* Nothing to do for anonymous RECORD type or no snapshot */
+	if (rec->rectypeid == RECORDOID || rec->nCompTypes <= 0)
+		return;
+
+	for (i = 0; i < rec->nCompTypes; i++)
+	{
+		TypeCacheEntry *typentry;
+
+		typentry = lookup_type_cache(rec->compTypeOids[i], TYPECACHE_TUPDESC);
+
+		if (typentry->tupDesc_identifier != rec->compTypeVersions[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("cannot return record variable \"%s\" after composite type \"%s\" was altered",
+							rec->refname,
+							format_type_be(rec->compTypeOids[i])),
+					 errhint("Reassign the record variable after ALTER TYPE.")));
+	}
+}
+
+/*
+ * collect_composite_type_versions
+ *
+ * Recursively collect tupDesc_identifier values for a composite type and
+ * all composite types reachable from its attributes.  Skips anonymous
+ * RECORD types and types already recorded (to prevent infinite recursion).
+ *
+ * oids/versions arrays are repalloc'd as needed; n/alloc updated in place.
+ */
+static void
+collect_composite_type_versions(Oid typid,
+								Oid **oids, uint64 **versions,
+								int *n, int *alloc)
+{
+	TypeCacheEntry *typentry;
+	TupleDesc	tupdesc;
+	int			i;
+
+	/* Skip if already recorded */
+	for (i = 0; i < *n; i++)
+	{
+		if ((*oids)[i] == typid)
+			return;
+	}
+
+	typentry = lookup_type_cache(typid, TYPECACHE_TUPDESC);
+
+	/* Grow arrays if needed */
+	if (*n >= *alloc)
+	{
+		*alloc *= 2;
+		*oids = repalloc(*oids, *alloc * sizeof(Oid));
+		*versions = repalloc(*versions, *alloc * sizeof(uint64));
+	}
+
+	(*oids)[*n] = typid;
+	(*versions)[*n] = typentry->tupDesc_identifier;
+	(*n)++;
+
+	tupdesc = typentry->tupDesc;
+	if (tupdesc == NULL)
+		return;
+
+	/* Recurse into composite-type attributes */
+	for (i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+		Oid			attrtypid;
+		char		typtype;
+
+		if (attr->attisdropped)
+			continue;
+
+		attrtypid = attr->atttypid;
+		if (attrtypid == RECORDOID)
+			continue;
+
+		typtype = get_typtype(attrtypid);
+
+		/* Resolve domain types to their base type */
+		if (typtype == TYPTYPE_DOMAIN)
+		{
+			attrtypid = getBaseType(attrtypid);
+			typtype = get_typtype(attrtypid);
+		}
+
+		if (typtype == TYPTYPE_COMPOSITE)
+			collect_composite_type_versions(attrtypid,
+										   oids, versions, n, alloc);
+	}
+}
+
+/*
+ * snapshot_record_composite_types
+ *
+ * Take a snapshot of tupDesc_identifier values for all composite types
+ * reachable from the record's declared type.  Called when a record variable
+ * is assigned a new value, so that check_record_type_not_altered() can
+ * detect mid-transaction ALTER TYPE at RETURN time.
+ */
+static void
+snapshot_record_composite_types(PLpgSQL_execstate *estate,
+								PLpgSQL_rec *rec)
+{
+	MemoryContext oldcxt;
+	int			alloc = 8;
+	int			n = 0;
+	Oid		   *oids;
+	uint64	   *versions;
+
+	/* Nothing to do for anonymous RECORD type */
+	if (rec->rectypeid == RECORDOID)
+	{
+		rec->nCompTypes = 0;
+		return;
+	}
+
+	oldcxt = MemoryContextSwitchTo(estate->datum_context);
+	oids = palloc(alloc * sizeof(Oid));
+	versions = palloc(alloc * sizeof(uint64));
+
+	collect_composite_type_versions(rec->rectypeid,
+									&oids, &versions, &n, &alloc);
+
+	MemoryContextSwitchTo(oldcxt);
+
+	if (n > 0)
+	{
+		/* Free previous snapshot if any */
+		if (rec->compTypeOids)
+			pfree(rec->compTypeOids);
+		if (rec->compTypeVersions)
+			pfree(rec->compTypeVersions);
+
+		rec->nCompTypes = n;
+		rec->compTypeOids = oids;
+		rec->compTypeVersions = versions;
+	}
+	else
+	{
+		pfree(oids);
+		pfree(versions);
+		rec->nCompTypes = 0;
+		rec->compTypeOids = NULL;
+		rec->compTypeVersions = NULL;
+	}
+}
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..cf9a657613d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -435,6 +435,15 @@ typedef struct PLpgSQL_rec
 
 	/* We always store record variables as "expanded" records */
 	ExpandedRecordHeader *erh;
+
+	/*
+	 * Composite type version snapshot for ALTER TYPE detection.
+	 * Populated when the record is assigned; checked at RETURN time.
+	 * Includes the outermost type and all nested composite types.
+	 */
+	int			nCompTypes;
+	Oid		   *compTypeOids;
+	uint64	   *compTypeVersions;
 } PLpgSQL_rec;
 
 /*
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 4fbed38b8bb..95f40e15b2f 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,61 @@ insert into two_int8s_tab values (compresult(42));
 -- reconnect so we lose any local knowledge of anonymous record types
 \c -
 table two_int8s_tab;
+
+-- Tests for bug #19382: server crash when ALTER TYPE is used mid-transaction
+-- in PL/pgSQL. Record variables populated before ALTER TYPE must not be
+-- returned, as the stored data no longer matches the current type definition.
+
+-- Case 1: Direct composite type change (INT -> TEXT)
+create type bug19382_foo as (a int, b int);
+create function bug19382_test_direct() returns record as $$
+declare r bug19382_foo := row(123, power(2, 30));
+begin
+    alter type bug19382_foo alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_direct();
+drop function bug19382_test_direct();
+drop type bug19382_foo cascade;
+
+-- Case 2: Nested composite type change
+create type bug19382_inner as (x int, y int);
+create type bug19382_outer as (a int, b bug19382_inner);
+create function bug19382_test_nested() returns record as $$
+declare r bug19382_outer;
+begin
+    r := row(1, row(10, power(2, 30)::int4)::bug19382_inner)::bug19382_outer;
+    alter type bug19382_inner alter attribute y type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_nested();
+drop function bug19382_test_nested();
+drop type bug19382_outer cascade;
+drop type bug19382_inner cascade;
+
+-- Case 3: OUT parameter
+create type bug19382_foo1 as (a int, b int);
+create function bug19382_test_out(out r1 bug19382_foo1) as $$
+begin
+    r1 := row(1, 2);
+    alter type bug19382_foo1 alter attribute b type text;
+    return;
+end;
+$$ language plpgsql;
+select bug19382_test_out();
+drop function bug19382_test_out();
+drop type bug19382_foo1 cascade;
+
+-- Case 4: No ALTER TYPE (baseline — must not error)
+create type bug19382_foo2 as (a int, b int);
+create function bug19382_test_baseline() returns bug19382_foo2 as $$
+declare r bug19382_foo2 := row(1, 2);
+begin
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_baseline();
+drop function bug19382_test_baseline();
+drop type bug19382_foo2;
-- 
2.39.5 (Apple Git-154)

