From 89fb897ffff093871069fafeceb16ce1d6486ffb Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:27:59 -0700
Subject: [PATCH v12 4/4] Detect composite types reached through container
 types (bug #19382)

The type-tree walk in collect_composite_type_versions() only recursed
into an attribute when its typtype was TYPTYPE_COMPOSITE.  A field
whose type is a container of composites -- for example t_comp[] -- has
typtype TYPTYPE_BASE (arrays are base types in PostgreSQL, not their
element type), so the recursion silently skipped it.

An ALTER TYPE on the element composite then went undetected, and
record_out() interpreted the stored bytes against the new tupdesc:

    CREATE TYPE t_comp AS (f1 int);
    CREATE TYPE t_parent AS (arr t_comp[]);
    CREATE FUNCTION ar() RETURNS t_parent AS $$ DECLARE r t_parent;
    BEGIN
        r := ROW(ARRAY[ROW(1073741824)::t_comp]::t_comp[])::t_parent;
        ALTER TYPE t_comp ALTER ATTRIBUTE f1 TYPE text;
        RETURN r;
    END; $$ LANGUAGE plpgsql;
    SELECT ar();               -- master and prior patches: crash

The failure is exactly the same class as the original bug 19382, one
container layer deeper: record_out on t_parent recurses into array_out
on t_comp[], which calls record_out on t_comp with the current (text)
tupdesc, which calls textout on the stored int -> segfault in
text_to_cstring.

Replace the single "if TYPTYPE_COMPOSITE" check with a peel loop that
unwraps domain and container layers (array, range, multirange) until
it reaches a composite type (which is snapshotted) or something that
cannot contain one (which is skipped).  The loop terminates because
each peel yields a strictly smaller type and PostgreSQL forbids
self-referential composites.
---
 .../plpgsql/src/expected/plpgsql_record.out   | 42 +++++++++++
 src/pl/plpgsql/src/pl_exec.c                  | 72 ++++++++++++++-----
 src/pl/plpgsql/src/plpgsql.h                  |  7 +-
 src/pl/plpgsql/src/sql/plpgsql_record.sql     | 37 ++++++++++
 4 files changed, 136 insertions(+), 22 deletions(-)

diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 1ac7cfd147c..8b77c99cfd5 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1127,3 +1127,45 @@ ERROR:  cannot return record variable "r" after composite type "bug19382_foo7" w
 CONTEXT:  PL/pgSQL function bug19382_test_generic_raise() line 6 at RAISE
 drop function bug19382_test_generic_raise();
 drop type bug19382_foo7 cascade;
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+    r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+    alter type bug19382_elem alter attribute f1 type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+ERROR:  cannot return record variable "r" after composite type "bug19382_elem" was altered
+CONTEXT:  PL/pgSQL function bug19382_test_array_elem() line 6 at RETURN
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+    r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+    alter type bug19382_elem2 add attribute f2 text;
+    r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+ bug19382_test_array_reassign 
+------------------------------
+ ("{""(1,hi)""}")
+(1 row)
+
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 073ace2d932..01645db8ca3 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -478,7 +478,6 @@ static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
 											PLpgSQL_rec *rec);
 static void refresh_erh_tupdesc_id(ExpandedRecordHeader *erh);
 
-
 /* ----------
  * plpgsql_exec_function	Called by the call handler for
  *				function execution.
@@ -5490,9 +5489,9 @@ exec_eval_datum(PLpgSQL_execstate *estate,
 				else
 				{
 					/*
-					 * Check if the record's composite type was altered since
-					 * the record was populated.  This catches all output
-					 * paths: RETURN, RAISE, EXECUTE USING, etc.
+					 * Check if the record's composite type was altered
+					 * since the record was populated.  This catches all
+					 * output paths: RETURN, RAISE, EXECUTE USING, etc.
 					 */
 					if (!ExpandedRecordIsEmpty(rec->erh))
 						check_record_type_not_altered(rec);
@@ -9282,7 +9281,7 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
 										 convert_value_to_string(estate,
 																 prm->value,
 																 prm->ptype),
-											 -1);
+										 -1);
 	}
 
 	MemoryContextSwitchTo(oldcontext);
@@ -9413,27 +9412,64 @@ collect_composite_type_versions(Oid typid,
 	{
 		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)
+		/*
+		 * A record field can reference a composite type directly, or wrap
+		 * one inside a domain or a container type (array, range, or
+		 * multirange).  ALTER TYPE on such an indirectly-referenced
+		 * composite must still be detected, so peel off domain and
+		 * container layers until we reach a composite type (which we
+		 * snapshot) or something that cannot contain one (which we skip).
+		 * This terminates because each peel step yields a strictly
+		 * "smaller" type.  Container element types cannot be self-referential.
+		 */
+		for (;;)
 		{
-			attrtypid = getBaseType(attrtypid);
-			typtype = get_typtype(attrtypid);
-		}
+			char		typtype = get_typtype(attrtypid);
+			Oid			elemtypid;
+
+			if (attrtypid == RECORDOID)
+				break;			/* anonymous rowtype: not versionable */
+
+			if (typtype == TYPTYPE_DOMAIN)
+			{
+				attrtypid = getBaseType(attrtypid);
+				continue;
+			}
 
-		if (typtype == TYPTYPE_COMPOSITE)
-			collect_composite_type_versions(attrtypid,
-											oids, versions, n, alloc);
+			if (typtype == TYPTYPE_COMPOSITE)
+			{
+				collect_composite_type_versions(attrtypid,
+												oids, versions, n, alloc);
+				break;
+			}
+
+			/* Array element type? */
+			elemtypid = get_element_type(attrtypid);
+
+			/* Otherwise a range's subtype? */
+			if (!OidIsValid(elemtypid))
+				elemtypid = get_range_subtype(attrtypid);
+
+			/* Otherwise a multirange's range's subtype? */
+			if (!OidIsValid(elemtypid) && typtype == TYPTYPE_MULTIRANGE)
+			{
+				Oid			rangetypid = get_multirange_range(attrtypid);
+
+				if (OidIsValid(rangetypid))
+					elemtypid = get_range_subtype(rangetypid);
+			}
+
+			if (!OidIsValid(elemtypid))
+				break;			/* not a composite-bearing type */
+
+			attrtypid = elemtypid;
+		}
 	}
 }
 
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index faa7b46274b..cf9a657613d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -437,10 +437,9 @@ typedef struct PLpgSQL_rec
 	ExpandedRecordHeader *erh;
 
 	/*
-	 * Snapshot of tupDesc_identifier values for all composite types reachable
-	 * from the record's declared type (or, for RECORDOID variables, from the
-	 * type adopted from the assigned value).  Used to detect mid-transaction
-	 * ALTER TYPE.  Empty for anonymous rowtypes that cannot be versioned.
+	 * 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;
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index dacf17678f6..5b97bedca39 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -793,3 +793,40 @@ $$ language plpgsql;
 select bug19382_test_generic_raise();
 drop function bug19382_test_generic_raise();
 drop type bug19382_foo7 cascade;
+
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+    r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+    alter type bug19382_elem alter attribute f1 type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+    r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+    alter type bug19382_elem2 add attribute f2 text;
+    r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
-- 
2.39.5 (Apple Git-154)

