public inbox for [email protected]  
help / color / mirror / Atom feed
Re: BUG #19382: Server crash at __nss_database_lookup
12+ messages / 3 participants
[nested] [flat]

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-03-19 05:00  surya poondla <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: surya poondla @ 2026-03-19 05:00 UTC (permalink / raw)
  To: [email protected]; [email protected]

Hi All,

I was able to reproduce the crash on laster master (19), the above patch
applies cleanly on postgres 19 and doesn't crash the server.

psql (19devel)
Type "help" for help.

postgres=# DROP FUNCTION IF EXISTS bar();
NOTICE:  function bar() does not exist, skipping
DROP FUNCTION
postgres=#   DROP TYPE IF EXISTS foo CASCADE;
NOTICE:  type "foo" does not exist, skipping
DROP TYPE
postgres=#   CREATE TYPE foo AS (a INT, b INT);
CREATE TYPE
postgres=#   CREATE FUNCTION bar() RETURNS RECORD AS $$
postgres$#   DECLARE
postgres$#       r foo := ROW(123, power(2, 30));
postgres$#   BEGIN
postgres$#       ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
postgres$#       RETURN r;
postgres$#   END;
postgres$#   $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# SELECT bar();
       bar
------------------
 (123,1073741824)
(1 row)

postgres=# DROP FUNCTION IF EXISTS bar1();
NOTICE:  function bar1() does not exist, skipping
DROP FUNCTION
postgres=#   DROP TYPE IF EXISTS foo1 CASCADE;
NOTICE:  type "foo1" does not exist, skipping
DROP TYPE
postgres=#   CREATE TYPE foo1 AS (a INT, b INT);
CREATE TYPE
postgres=#   CREATE FUNCTION bar1(OUT r1 foo1) AS $$
postgres$#   BEGIN
postgres$#       r1 := ROW(1, 2);
postgres$#       ALTER TYPE foo1 ALTER ATTRIBUTE b TYPE TEXT;
postgres$#       RETURN;
postgres$#   END;
postgres$#   $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# SELECT bar1();
 bar1
-------
 (1,2)
(1 row)

postgres=# DROP FUNCTION IF EXISTS bar2();
NOTICE:  function bar2() does not exist, skipping
DROP FUNCTION
postgres=#   DROP TYPE IF EXISTS foo2 CASCADE;
NOTICE:  type "foo2" does not exist, skipping
DROP TYPE
postgres=#   CREATE TYPE foo2 AS (a INT, b TEXT);
CREATE TYPE
postgres=#   CREATE FUNCTION bar2() RETURNS foo2 AS $$
postgres$#   DECLARE
postgres$#       r foo2 := ROW(1, 'hello');
postgres$#   BEGIN
postgres$#       ALTER TYPE foo2 ALTER ATTRIBUTE b TYPE INT;
postgres$#       RETURN r;
postgres$#   END;
postgres$#   $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=#   SELECT bar2();
ERROR:  invalid input syntax for type integer: "hello"
CONTEXT:  PL/pgSQL function bar2() line 6 at RETURN
postgres=# DROP FUNCTION bar();
DROP FUNCTION
postgres=#   DROP FUNCTION bar1();
DROP FUNCTION
postgres=#   DROP FUNCTION bar2();
DROP FUNCTION
postgres=#   DROP TYPE IF EXISTS foo CASCADE;
DROP TYPE
postgres=#   DROP TYPE IF EXISTS foo1 CASCADE;
DROP TYPE
postgres=#   DROP TYPE IF EXISTS foo2 CASCADE;
DROP TYPE
postgres=# quit

Regards,
Surya Poondla


Attachments:

  [application/octet-stream] 0003-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m_PG19.patch (7.0K, 3-0003-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m_PG19.patch)
  download | inline diff:
From 3289746d88fbca10712d43cc4f50fa2ce99e62c1 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v3] 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 | 173 ++++++++++++++++++++++++++++++++++-
 1 file changed, 172 insertions(+), 1 deletion(-)

diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 6b077febdc8..0d85a95d795 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -458,6 +458,8 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
 static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
 											  PLpgSQL_expr *expr);
 
+static void convert_record_for_altered_type(PLpgSQL_execstate *estate,
+											PLpgSQL_rec *rec);
 
 /* ----------
  * plpgsql_exec_function	Called by the call handler for
@@ -3244,8 +3246,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, convert the data to
+					 * prevent crashes when outputting the record.
+					 */
+					if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+						!ExpandedRecordIsEmpty(rec->erh))
+						convert_record_for_altered_type(estate, 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;
@@ -3390,6 +3414,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, convert the data to
+					 * prevent crashes when storing to the tuplestore.
+					 */
+					if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+						convert_record_for_altered_type(estate, rec);
+
 					/* If rec is null, try to convert it to a row of nulls */
 					if (rec->erh == NULL)
 						instantiate_empty_record_variable(estate, rec);
@@ -8883,3 +8915,142 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
 
 	return paramstr.data;
 }
+
+/*
+ * convert_record_for_altered_type
+ *
+ * Check if a record's composite type has been altered since the record
+ * was populated, and if so, convert the record data to match the new
+ * type definition. This prevents crashes that can occur when the stored
+ * data doesn't match the current type definition.
+ *
+ * If conversion is needed, assigns the new record to rec via
+ * assign_record_var(), which transfers it to datum_context and frees
+ * the old record.
+ */
+static void
+convert_record_for_altered_type(PLpgSQL_execstate *estate,
+								PLpgSQL_rec *rec)
+{
+	ExpandedRecordHeader *erh = rec->erh;
+	Oid				rectypeid = rec->rectypeid;
+	TupleDesc		old_tupdesc;
+	TupleDesc		new_tupdesc;
+	TypeCacheEntry *typentry;
+	uint64			current_tupdesc_id;
+	ExpandedRecordHeader *new_erh;
+	Datum		   *old_values;
+	bool		   *old_nulls;
+	Datum		   *new_values;
+	bool		   *new_nulls;
+	int				natts;
+	int				i;
+	MemoryContext	oldcxt;
+	bool			need_conversion = false;
+
+	/* Nothing to do for anonymous RECORD type */
+	if (rectypeid == RECORDOID)
+		return;
+
+	/* Get current type definition from typcache */
+	typentry = lookup_type_cache(rectypeid,
+								 TYPECACHE_TUPDESC |
+								 TYPECACHE_DOMAIN_BASE_INFO);
+	if (typentry->typtype == TYPTYPE_DOMAIN)
+		typentry = lookup_type_cache(typentry->domainBaseType,
+									 TYPECACHE_TUPDESC);
+
+	current_tupdesc_id = typentry->tupDesc_identifier;
+
+	/* If type hasn't changed, nothing to do (fast path) */
+	if (erh->er_tupdesc_id == current_tupdesc_id)
+		return;
+
+	/*
+	 * Type version has changed. Need to check if field types actually differ
+	 * and convert if necessary.
+	 */
+	old_tupdesc = erh->er_tupdesc;
+	new_tupdesc = typentry->tupDesc;
+
+	/* Sanity check: must have same number of attributes */
+	if (old_tupdesc->natts != new_tupdesc->natts)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("record type \"%s\" structure has changed",
+						format_type_be(rectypeid)),
+				 errdetail("Number of columns changed from %d to %d.",
+						   old_tupdesc->natts, new_tupdesc->natts)));
+
+	natts = old_tupdesc->natts;
+
+	/* Deconstruct the old record to access field values */
+	deconstruct_expanded_record(erh);
+	old_values = erh->dvalues;
+	old_nulls = erh->dnulls;
+
+	/* Allocate arrays for new values */
+	oldcxt = MemoryContextSwitchTo(get_eval_mcontext(estate));
+	new_values = (Datum *) palloc(natts * sizeof(Datum));
+	new_nulls = (bool *) palloc(natts * sizeof(bool));
+	MemoryContextSwitchTo(oldcxt);
+
+	/* Convert each field */
+	for (i = 0; i < natts; i++)
+	{
+		Form_pg_attribute old_att = TupleDescAttr(old_tupdesc, i);
+		Form_pg_attribute new_att = TupleDescAttr(new_tupdesc, i);
+
+		/* Skip dropped columns */
+		if (old_att->attisdropped || new_att->attisdropped)
+		{
+			new_values[i] = (Datum) 0;
+			new_nulls[i] = true;
+			continue;
+		}
+
+		/* If null, stays null */
+		if (old_nulls[i])
+		{
+			new_values[i] = (Datum) 0;
+			new_nulls[i] = true;
+			continue;
+		}
+
+		/* If same type, no conversion needed */
+		if (old_att->atttypid == new_att->atttypid &&
+			(old_att->atttypmod == new_att->atttypmod ||
+			 new_att->atttypmod == -1))
+		{
+			new_values[i] = old_values[i];
+			new_nulls[i] = false;
+			continue;
+		}
+
+		/* Different type: convert using exec_cast_value */
+		need_conversion = true;
+		new_nulls[i] = false;
+		new_values[i] = exec_cast_value(estate,
+										old_values[i],
+										&new_nulls[i],
+										old_att->atttypid,
+										old_att->atttypmod,
+										new_att->atttypid,
+										new_att->atttypmod);
+	}
+
+	/* If no actual conversion was needed, return without modifying rec */
+	if (!need_conversion)
+		return;
+
+	/* Build new expanded record with converted values */
+	new_erh = make_expanded_record_from_typeid(rectypeid, -1,
+											   get_eval_mcontext(estate));
+	expanded_record_set_fields(new_erh, new_values, new_nulls, true);
+
+	/*
+	 * Assign the new record to rec, transferring it to datum_context
+	 * and freeing the old record.
+	 */
+	assign_record_var(estate, rec, new_erh);
+}
-- 
2.39.5 (Apple Git-154)



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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-03-19 07:53  =?utf-8?B?c29uZ2ppbnpob3U=?= <[email protected]>
  parent: surya poondla <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: =?utf-8?B?c29uZ2ppbnpob3U=?= @ 2026-03-19 07:53 UTC (permalink / raw)
  To: =?utf-8?B?c3VyeWEgcG9vbmRsYQ==?= <[email protected]>; =?utf-8?B?ZGxsZ2d5eA==?= <[email protected]>; =?utf-8?B?cGdzcWwtYnVncw==?= <[email protected]>

Hi Surya Poondla:

After applying the patch on the master branch, I debugged it and the type mismatch issue is indeed not as bad as before. I haven't looked at this patch much, but I'm a little worried about performance issues here.


I have one more question: Can we iterate through the code first to get the value of `need_conversion`, and then allocate memory and perform subsequent operations only if necessary? Of course, this is just my opinion. Thank you.


Regards,
songjinzhou




songjinzhou
[email protected]



        



         原始邮件
         
       
发件人:surya poondla <[email protected]&gt;
发件时间:2026年3月19日 13:00
收件人:dllggyx <[email protected]&gt;, pgsql-bugs <[email protected]&gt;
主题:Re: BUG #19382: Server crash at __nss_database_lookup



Hi All,

I was able to reproduce the crash on laster master (19), the above patch applies cleanly on postgres 19 and doesn't crash the server.

psql (19devel)
Type "help" for help.

postgres=# DROP FUNCTION IF EXISTS bar();
NOTICE: &nbsp;function bar() does not exist, skipping
DROP FUNCTION
postgres=# &nbsp; DROP TYPE IF EXISTS foo CASCADE;
NOTICE: &nbsp;type "foo" does not exist, skipping
DROP TYPE
postgres=# &nbsp; CREATE TYPE foo AS (a INT, b INT);
CREATE TYPE
postgres=# &nbsp; CREATE FUNCTION bar() RETURNS RECORD AS $$
postgres$# &nbsp; DECLARE
postgres$# &nbsp; &nbsp; &nbsp; r foo := ROW(123, power(2, 30));
postgres$# &nbsp; BEGIN
postgres$# &nbsp; &nbsp; &nbsp; ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
postgres$# &nbsp; &nbsp; &nbsp; RETURN r;
postgres$# &nbsp; END;
postgres$# &nbsp; $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# SELECT bar();
&nbsp; &nbsp; &nbsp; &nbsp;bar
------------------
&nbsp;(123,1073741824)
(1 row)

postgres=# DROP FUNCTION IF EXISTS bar1();
NOTICE: &nbsp;function bar1() does not exist, skipping
DROP FUNCTION
postgres=# &nbsp; DROP TYPE IF EXISTS foo1 CASCADE;
NOTICE: &nbsp;type "foo1" does not exist, skipping
DROP TYPE
postgres=# &nbsp; CREATE TYPE foo1 AS (a INT, b INT);
CREATE TYPE
postgres=# &nbsp; CREATE FUNCTION bar1(OUT r1 foo1) AS $$
postgres$# &nbsp; BEGIN
postgres$# &nbsp; &nbsp; &nbsp; r1 := ROW(1, 2);
postgres$# &nbsp; &nbsp; &nbsp; ALTER TYPE foo1 ALTER ATTRIBUTE b TYPE TEXT;
postgres$# &nbsp; &nbsp; &nbsp; RETURN;
postgres$# &nbsp; END;
postgres$# &nbsp; $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# SELECT bar1();
&nbsp;bar1
-------
&nbsp;(1,2)
(1 row)

postgres=# DROP FUNCTION IF EXISTS bar2();
NOTICE: &nbsp;function bar2() does not exist, skipping
DROP FUNCTION
postgres=# &nbsp; DROP TYPE IF EXISTS foo2 CASCADE;
NOTICE: &nbsp;type "foo2" does not exist, skipping
DROP TYPE
postgres=# &nbsp; CREATE TYPE foo2 AS (a INT, b TEXT);
CREATE TYPE
postgres=# &nbsp; CREATE FUNCTION bar2() RETURNS foo2 AS $$
postgres$# &nbsp; DECLARE
postgres$# &nbsp; &nbsp; &nbsp; r foo2 := ROW(1, 'hello');
postgres$# &nbsp; BEGIN
postgres$# &nbsp; &nbsp; &nbsp; ALTER TYPE foo2 ALTER ATTRIBUTE b TYPE INT;
postgres$# &nbsp; &nbsp; &nbsp; RETURN r;
postgres$# &nbsp; END;
postgres$# &nbsp; $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# &nbsp; SELECT bar2();
ERROR: &nbsp;invalid input syntax for type integer: "hello"
CONTEXT: &nbsp;PL/pgSQL function bar2() line 6 at RETURN
postgres=# DROP FUNCTION bar();
DROP FUNCTION
postgres=# &nbsp; DROP FUNCTION bar1();
DROP FUNCTION
postgres=# &nbsp; DROP FUNCTION bar2();
DROP FUNCTION
postgres=# &nbsp; DROP TYPE IF EXISTS foo CASCADE;
DROP TYPE
postgres=# &nbsp; DROP TYPE IF EXISTS foo1 CASCADE;
DROP TYPE
postgres=# &nbsp; DROP TYPE IF EXISTS foo2 CASCADE;
DROP TYPE
postgres=# quit

Regards,
Surya Poondla

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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-03-20 18:16  surya poondla <[email protected]>
  parent: =?utf-8?B?c29uZ2ppbnpob3U=?= <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: surya poondla @ 2026-03-20 18:16 UTC (permalink / raw)
  To: songjinzhou <[email protected]>; +Cc: dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi Songjinzhou,

Thank you for reviewing the patch.

You're correct that there is a minor inefficiency, once a
tupDesc_identifier version mismatch is detected, the patch allocates
new_values/new_nulls arrays and calls deconstruct_expanded_record() before
knowing whether any field types actually differ.
If the version changed but no field types changed (e.g., a constraint-only
ALTER), we do unnecessary work and hit the if (!need_conversion) return
late.

That said, your suggestion is clean and correct. I can restructure the code
to do a first pass over the TupleDesc attributes (which is pure metadata,
no deconstruction needed) to set need_conversion,
and only proceed with deconstruct_expanded_record() and array allocation if
that returns true. This avoids any unnecessary memory allocation in that
intermediate case.

I'll post an updated patch with this improvement.

Thanks again for the careful review!

Regards,
Surya Poondla


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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-02 11:18  Andrey Borodin <[email protected]>
  parent: surya poondla <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: Andrey Borodin @ 2026-04-02 11:18 UTC (permalink / raw)
  To: surya poondla <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi!

Thanks for working on this!

> On 20 Mar 2026, at 23:16, surya poondla <[email protected]> wrote:
> 
> I'll post an updated patch with this improvement.

After your patch Postgres still crashes on this test:

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;

This test case was proposed in this thread, but I suggest treating this as a separate bug needing separate fix.


In my opinion in both cases (PL/pgSQL + CURSOR) we should error out instead of trying to remediate type changes.


Best regards, Andrey Borodin.





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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-02 23:14  surya poondla <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: surya poondla @ 2026-04-02 23:14 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi All,

Thanks for the review Andrey.

On Thu, Apr 2, 2026 at 4:18 AM Andrey Borodin <[email protected]> wrote:

> Hi!
>
> Thanks for working on this!
>
> > On 20 Mar 2026, at 23:16, surya poondla <[email protected]> wrote:
> >
> > I'll post an updated patch with this improvement.
>
> After your patch Postgres still crashes on this test:
>
> 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;
>
> This test case was proposed in this thread, but I suggest treating this as
> a separate bug needing separate fix.
>

Thank you for reporting this. Yes the cursor case can be treated as a
separate bug.
Though the 2 crash scenarios have the same root cause (record_out()
interpreting old data with new type definition) they require different fix
requirements.
1. PL/pgSQL case (this patch): ExpandedRecords already carry er_tupdesc_id
the version tracking infrastructure exists. The fix detects the mismatch
and converts the data. This is a self-contained bug fix using existing
mechanisms.
2. Cursor case: Flat HeapTuples carry no type version information, they
only have the type OID, which doesn't change after ALTER TYPE. Fixing this
requires adding new infrastructure that PostgreSQL doesn't have today
(e.g., storing tupDesc_identifier in Portal structures, or adding version
fields to HeapTupleHeaders). This is a broader architectural change that
affects core structures like PortalData, pquery.c, and potentially
portalmem.c. We need to see how to add version tracking to composite-type
values. I will work on this fix in parallel.



> In my opinion in both cases (PL/pgSQL + CURSOR) we should error out
> instead of trying to remediate type changes.
>
> I've simplified the fix. Instead of converting the record data, we now
raise a clear error when a composite type is altered mid-transaction after
the record was populated.
This also addresses the performance concern raised earlier since there's no
conversion logic at all now.



Updated patch attached.

Regards,
Surya Poondla


Attachments:

  [application/octet-stream] 0004-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (4.2K, 3-0004-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
  download | inline diff:
From 5c8867d4230beeb1a3d12cf45d699ea9ad027180 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
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)



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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-04 12:42  Andrey Borodin <[email protected]>
  parent: surya poondla <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: Andrey Borodin @ 2026-04-04 12:42 UTC (permalink / raw)
  To: surya poondla <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>



> On 3 Apr 2026, at 04:14, surya poondla <[email protected]> wrote:
> 
> <0004-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch>

Hi Surya,

Thanks for the updated patch. I noticed it checks er_tupdesc_id of the
outermost record variable, but does not recurse into nested composite types.
The server still crashes when only an inner type is altered:

CREATE TYPE inner_t AS (x INT, y INT);
CREATE TYPE outer_t AS (a INT, b inner_t);

CREATE OR REPLACE FUNCTION test_nested() RETURNS record LANGUAGE plpgsql AS $$
DECLARE r1 outer_t; r2 outer_t;
BEGIN
    r1 := ROW(1, ROW(10, power(2,30)::int4)::inner_t)::outer_t;
    ALTER TYPE inner_t ALTER ATTRIBUTE y TYPE TEXT;
    r2 := r1;
    RETURN r2;
END; $$;

SELECT test_nested();   -- server crash


The same gap exists on the cursor side independently of your patch, and I
have a fix for that part that walks the type tree recursively. IMO the
PL/pgSQL assignment path will need a similar recursive check.

I'm definitely not a big fan of checking types on every FETCH, but I see no
other ways around.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v2026-04-04-0001-Fix-incorrect-results-when-composite-typ.patch (9.4K, 2-v2026-04-04-0001-Fix-incorrect-results-when-composite-typ.patch)
  download | inline diff:
From bd2f4aff350aab947ab66cdad7afa910f1ede7a1 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <[email protected]>
Date: Sat, 4 Apr 2026 10:26:44 +0500
Subject: [PATCH v2026-04-04] Fix incorrect results when composite type is
 altered mid-cursor-scan

HeapTuples carry only the type OID, not a schema version.  If ALTER TYPE
changes a composite type between FETCHes, the stored tuples are
interpreted with the wrong definition.

At cursor open, record the tupDesc_identifier of every composite type
reachable from the result columns (including nested ones).  Reject the
FETCH with ERRCODE_INVALID_CURSOR_STATE if any identifier has changed.

Reported-by: Yuxiao Guo <[email protected]>
Discussion: https://www.postgresql.org/message-id/CAOVWO5oRGPd7mA3d85jNYmjLNfeBAca5oDcHTfRFxbAwPLxs5g@mail.gmail.com
---
 src/backend/tcop/pquery.c              | 130 +++++++++++++++++++++++++
 src/include/utils/portal.h             |   5 +
 src/test/regress/expected/rowtypes.out |  54 ++++++++++
 src/test/regress/sql/rowtypes.sql      |  36 +++++++
 4 files changed, 225 insertions(+)

diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index d8fc75d0bb9..07cbf288936 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -25,8 +25,12 @@
 #include "pg_trace.h"
 #include "tcop/pquery.h"
 #include "tcop/utility.h"
+#include "catalog/pg_type_d.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/snapmgr.h"
+#include "utils/typcache.h"
 
 
 /*
@@ -425,6 +429,103 @@ FetchStatementTargetList(Node *stmt)
  * On return, portal is ready to accept PortalRun() calls, and the result
  * tupdesc (if any) is known.
  */
+
+/*
+ * CollectCompositeTypeVersions
+ *		Record typid's tupDesc_identifier, then recurse into its composite-type
+ *		attributes.  Duplicate OIDs are skipped.  Arrays are repalloc'd as
+ *		needed; n/alloc are updated in place.
+ */
+static void
+CollectCompositeTypeVersions(Oid typid,
+							 Oid **oids, uint64 **versions,
+							 int *n, int *alloc)
+{
+	TypeCacheEntry *typentry;
+	TupleDesc	tupdesc;
+
+	for (int i = 0; i < *n; i++)	/* skip if already recorded */
+		if ((*oids)[i] == typid)
+			return;
+
+	typentry = lookup_type_cache(typid, TYPECACHE_TUPDESC);
+
+	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;
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+		if (!attr->attisdropped &&
+			attr->atttypid != RECORDOID &&
+			get_typtype(attr->atttypid) == TYPTYPE_COMPOSITE)
+			CollectCompositeTypeVersions(attr->atttypid,
+										 oids, versions, n, alloc);
+	}
+}
+
+/*
+ * InitPortalCompositeTypeVersions
+ *		Snapshot tupDesc_identifier for every named composite type reachable
+ *		from portal->tupDesc (including nested types).  Called once at cursor
+ *		open; checked at each FETCH to detect mid-scan ALTER TYPE.
+ */
+static void
+InitPortalCompositeTypeVersions(Portal portal)
+{
+	TupleDesc	tupdesc = portal->tupDesc;
+	MemoryContext oldcxt;
+	int			alloc = 8;
+	int			n = 0;
+	Oid		   *oids;
+	uint64	   *versions;
+
+	if (tupdesc == NULL)
+		return;
+
+	oldcxt = MemoryContextSwitchTo(portal->portalContext);
+	oids = palloc(alloc * sizeof(Oid));
+	versions = palloc(alloc * sizeof(uint64));
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+		if (!attr->attisdropped &&
+			attr->atttypid != RECORDOID &&
+			get_typtype(attr->atttypid) == TYPTYPE_COMPOSITE)
+			CollectCompositeTypeVersions(attr->atttypid,
+										 &oids, &versions, &n, &alloc);
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+
+	if (n > 0)
+	{
+		portal->nCursorCompositeTypes = n;
+		portal->cursorCompositeTypeOids = oids;
+		portal->cursorCompositeTypeVersions = versions;
+	}
+	else
+	{
+		pfree(oids);
+		pfree(versions);
+	}
+}
+
 void
 PortalStart(Portal portal, ParamListInfo params,
 			int eflags, Snapshot snapshot)
@@ -522,6 +623,13 @@ PortalStart(Portal portal, ParamListInfo params,
 				 */
 				portal->tupDesc = queryDesc->tupDesc;
 
+				/*
+				 * Record type-cache versions for any named composite-type
+				 * result columns so that FETCH can detect mid-scan ALTER
+				 * TYPE.
+				 */
+				InitPortalCompositeTypeVersions(portal);
+
 				/*
 				 * Reset cursor position data to "start of query"
 				 */
@@ -1383,6 +1491,28 @@ PortalRunFetch(Portal portal,
 
 	Assert(PortalIsValid(portal));
 
+	/*
+	 * Reject the fetch if any composite type in the result has been altered
+	 * since the cursor was opened; HeapTuples carry no type-version tag so
+	 * the mismatch cannot be caught later.
+	 */
+	if (portal->nCursorCompositeTypes > 0)
+	{
+		for (int i = 0; i < portal->nCursorCompositeTypes; i++)
+		{
+			Oid			typid = portal->cursorCompositeTypeOids[i];
+			TypeCacheEntry *typentry =
+				lookup_type_cache(typid, TYPECACHE_TUPDESC);
+
+			if (typentry->tupDesc_identifier != portal->cursorCompositeTypeVersions[i])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_CURSOR_STATE),
+						 errmsg("cursor scan cannot continue after composite type \"%s\" was altered",
+								format_type_be(typid)),
+						 errhint("Close and reopen the cursor after ALTER TYPE.")));
+		}
+	}
+
 	/*
 	 * Check for improper portal use, and mark portal active.
 	 */
diff --git a/src/include/utils/portal.h b/src/include/utils/portal.h
index a7bedb12c18..a8d725a35ad 100644
--- a/src/include/utils/portal.h
+++ b/src/include/utils/portal.h
@@ -160,6 +160,11 @@ typedef struct PortalData
 	/* and these are the format codes to use for the columns: */
 	int16	   *formats;		/* a format code for each column */
 
+	/* tupDesc_identifier snapshots for composite types in the result columns */
+	int			nCursorCompositeTypes;	/* 0 if none */
+	Oid		   *cursorCompositeTypeOids;
+	uint64	   *cursorCompositeTypeVersions;
+
 	/*
 	 * Outermost ActiveSnapshot for execution of the portal's queries.  For
 	 * all but a few utility commands, we require such a snapshot to exist.
diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out
index 956bc2d02fc..7f4c0567fce 100644
--- a/src/test/regress/expected/rowtypes.out
+++ b/src/test/regress/expected/rowtypes.out
@@ -1408,3 +1408,57 @@ ERROR:  column "oid" not found in data type compositetable
 LINE 1: SELECT (NULL::compositetable).oid;
                 ^
 DROP TABLE compositetable;
+-- ALTER TYPE mid-cursor-scan must be detected and raise an error.
+CREATE TYPE mycomptype AS (a INT, b INT);
+BEGIN;
+DECLARE cur1 CURSOR FOR
+    SELECT (i, i * 100)::mycomptype FROM generate_series(1, 5) i;
+FETCH cur1;
+   row   
+---------
+ (1,100)
+(1 row)
+
+ALTER TYPE mycomptype ALTER ATTRIBUTE b TYPE TEXT;
+FETCH cur1;
+ERROR:  cursor scan cannot continue after composite type "mycomptype" was altered
+HINT:  Close and reopen the cursor after ALTER TYPE.
+COMMIT;
+DROP TYPE mycomptype;
+-- Same check applies when a nested composite type is altered.
+CREATE TYPE myinnertype AS (x INT, y INT);
+CREATE TYPE myoutertype AS (a INT, b myinnertype);
+BEGIN;
+DECLARE cur2 CURSOR FOR
+    SELECT (i, (i * 10, i * 100)::myinnertype)::myoutertype
+    FROM generate_series(1, 5) i;
+FETCH cur2;
+      row       
+----------------
+ (1,"(10,100)")
+(1 row)
+
+ALTER TYPE myinnertype ALTER ATTRIBUTE y TYPE TEXT;
+FETCH cur2;
+ERROR:  cursor scan cannot continue after composite type "myinnertype" was altered
+HINT:  Close and reopen the cursor after ALTER TYPE.
+COMMIT;
+DROP TYPE myoutertype;
+DROP TYPE myinnertype;
+-- MOVE goes through the same portal path and must also be rejected.
+CREATE TYPE mycomptype2 AS (a INT, b INT);
+BEGIN;
+DECLARE cur3 CURSOR FOR
+    SELECT (i, i)::mycomptype2 FROM generate_series(1, 10) i;
+FETCH cur3;
+  row  
+-------
+ (1,1)
+(1 row)
+
+ALTER TYPE mycomptype2 ALTER ATTRIBUTE b TYPE TEXT;
+MOVE cur3;
+ERROR:  cursor scan cannot continue after composite type "mycomptype2" was altered
+HINT:  Close and reopen the cursor after ALTER TYPE.
+COMMIT;
+DROP TYPE mycomptype2;
diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql
index 174b062144a..4fad2bc1719 100644
--- a/src/test/regress/sql/rowtypes.sql
+++ b/src/test/regress/sql/rowtypes.sql
@@ -562,3 +562,39 @@ SELECT (NULL::compositetable).a;
 SELECT (NULL::compositetable).oid;
 
 DROP TABLE compositetable;
+
+-- ALTER TYPE mid-cursor-scan must be detected and raise an error.
+CREATE TYPE mycomptype AS (a INT, b INT);
+BEGIN;
+DECLARE cur1 CURSOR FOR
+    SELECT (i, i * 100)::mycomptype FROM generate_series(1, 5) i;
+FETCH cur1;
+ALTER TYPE mycomptype ALTER ATTRIBUTE b TYPE TEXT;
+FETCH cur1;
+COMMIT;
+DROP TYPE mycomptype;
+
+-- Same check applies when a nested composite type is altered.
+CREATE TYPE myinnertype AS (x INT, y INT);
+CREATE TYPE myoutertype AS (a INT, b myinnertype);
+BEGIN;
+DECLARE cur2 CURSOR FOR
+    SELECT (i, (i * 10, i * 100)::myinnertype)::myoutertype
+    FROM generate_series(1, 5) i;
+FETCH cur2;
+ALTER TYPE myinnertype ALTER ATTRIBUTE y TYPE TEXT;
+FETCH cur2;
+COMMIT;
+DROP TYPE myoutertype;
+DROP TYPE myinnertype;
+
+-- MOVE goes through the same portal path and must also be rejected.
+CREATE TYPE mycomptype2 AS (a INT, b INT);
+BEGIN;
+DECLARE cur3 CURSOR FOR
+    SELECT (i, i)::mycomptype2 FROM generate_series(1, 10) i;
+FETCH cur3;
+ALTER TYPE mycomptype2 ALTER ATTRIBUTE b TYPE TEXT;
+MOVE cur3;
+COMMIT;
+DROP TYPE mycomptype2;
-- 
2.51.2



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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-09 04:24  surya poondla <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 2 replies; 12+ messages in thread

From: surya poondla @ 2026-04-09 04:24 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi Andrey,

Thank you for the comments.


> The server still crashes when only an inner type is altered:
>
> CREATE TYPE inner_t AS (x INT, y INT);
> CREATE TYPE outer_t AS (a INT, b inner_t);
>
> CREATE OR REPLACE FUNCTION test_nested() RETURNS record LANGUAGE plpgsql
> AS $$
> DECLARE r1 outer_t; r2 outer_t;
> BEGIN
>     r1 := ROW(1, ROW(10, power(2,30)::int4)::inner_t)::outer_t;
>     ALTER TYPE inner_t ALTER ATTRIBUTE y TYPE TEXT;
>     r2 := r1;
>     RETURN r2;
> END; $$;
>
> SELECT test_nested();   -- server crash
>
> Thank you for the nested composite testcase and the fix in cursor
code, the changes look good.. I fixed the PL/pgsql to fix the nested
components.

Here is the patch for the pl/pgsql fix

Regards,
Surya Poondla


Attachments:

  [application/octet-stream] 0005-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (14.2K, 3-0005-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
  download | inline diff:
From 2d83dde32a3a94b7298399c3f47c38f702cc96e7 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
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)



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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-14 23:49  surya poondla <[email protected]>
  parent: surya poondla <[email protected]>
  1 sibling, 0 replies; 12+ messages in thread

From: surya poondla @ 2026-04-14 23:49 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi All,

The latest CFBot run (v5 patch) shows one CI failure: FreeBSD - Meson:
test_misc/007_catcache_inval exits with status 29.
This failure is unrelated to the patch.
The failing test exercises catalog cache invalidation using injection
points on SQL function lookups, no PL/pgSQL, no record variables, no
composite type handling.

The patch only touches assign_record_var() in pl_exec.c and PLpgSQL_rec in
plpgsql.h, neither of which are executed by this test. All PL/pgSQL
regression tests pass on all platforms.

Re-attaching the patch to kick in the CFBot again.

Regards,
Surya Poondla


Attachments:

  [application/octet-stream] 0005-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (14.2K, 3-0005-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
  download | inline diff:
From 2d83dde32a3a94b7298399c3f47c38f702cc96e7 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
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)



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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-16 12:00  Andrey Borodin <[email protected]>
  parent: surya poondla <[email protected]>
  1 sibling, 1 reply; 12+ messages in thread

From: Andrey Borodin @ 2026-04-16 12:00 UTC (permalink / raw)
  To: surya poondla <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>



> On 9 Apr 2026, at 09:24, surya poondla <[email protected]> wrote:
> 
> <0005-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch>

I’ve took a look into the patch and here are some thoughts:

1. collect_composite_type_versions() does this
/* Resolve domain types to their base type */
if (typtype == TYPTYPE_DOMAIN)
{
			attrtypid = getBaseType(attrtypid);
			typtype = get_typtype(attrtypid);
}

Only for nested types. Do we need this for root?

2. When we do this

/* Clear composite type snapshot */
rec->nCompTypes = 0;
rec->compTypeOids = NULL;
rec->compTypeVersions = NULL;

We also might need two pfree()s.

3. Here are few other test cases that crash with the patch.

-- Case 5: Dot assignment
create type bug19382_foo3 as (a int, b int);
create function bug19382_test_field_assign() returns record as $$
declare r bug19382_foo3;
begin
r.a := 123;
r.b := power(2, 30)::int4;
alter type bug19382_foo3 alter attribute b type text;
return r;
end;
$$ language plpgsql;
select bug19382_test_field_assign();
drop function bug19382_test_field_assign();
drop type bug19382_foo3 cascade;

-- Case 6: SELECT INTO field also bypasses snapshot.
create type bug19382_foo4 as (a int, b int);
create table bug19382_tbl (a int, b int);
insert into bug19382_tbl values (123, power(2, 30)::int4);
create function bug19382_test_select_into_field() returns record as $$
declare r bug19382_foo4;
begin
select a, b into r.a, r.b from bug19382_tbl;
alter type bug19382_foo4 alter attribute b type text;
return r;
end;
$$ language plpgsql;
select bug19382_test_select_into_field();
drop function bug19382_test_select_into_field();
drop table bug19382_tbl;
drop type bug19382_foo4 cascade;

Thanks!

Best regards, Andrey Borodin.





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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-16 23:20  surya poondla <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: surya poondla @ 2026-04-16 23:20 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi Andrey,

Thank you for the detailed review and test cases. I've addressed all three
points in v6:

Point 1 (domain at root): collect_composite_type_versions() now resolves
domain types at the root level, not just for nested attributes. This
ensures the type tree walk works correctly when the record variable itself
is a domain over composite.

Point 2 (missing pfree): Added pfree() calls for compTypeOids and
compTypeVersions before NULLing them in the record-clear path.

Point 3 (Cases 5 and 6 i.e dot assignment and SELECT INTO fields): These
bypassed assign_record_var() because they modify the ExpandedRecord in
place via expanded_record_set_field(). The fix was to restructure
check_record_type_not_altered() into a two-level check:
  i) Outermost type: Always checked using erh->er_tupdesc_id, which is set
when the ExpandedRecord is created. This works for all code paths (whole
assignment, field assignment, SELECT INTO) without needing a snapshot.
  ii) Nested types: Checked against the snapshot when available (taken at
assign_record_var() and instantiate_empty_record_variable()).

I also added a fast-path optimization in snapshot_record_composite_types()
to avoid repeated type tree walks when a record is assigned in a loop — it
skips the snapshot if the outermost type's tupDesc_identifier hasn't
changed since the last snapshot.

Added both new test cases (Cases 5 and 6) to the regression. All 248 core
regression tests and all 13 PL/pgSQL tests pass.

Regards,
Surya Poondla


Attachments:

  [application/octet-stream] 0006-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (18.8K, 3-0006-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
  download | inline diff:
From 7794389e88a5072ede296dd6d6e101e9e8e32ba8 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v6] 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   |  98 +++++++
 src/pl/plpgsql/src/pl_exec.c                  | 265 +++++++++++++++++-
 src/pl/plpgsql/src/plpgsql.h                  |   9 +
 src/pl/plpgsql/src/sql/plpgsql_record.sql     |  90 ++++++
 4 files changed, 461 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..b2e12946c5d 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,101 @@ 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
+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
+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
+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;
+-- Case 5: Field-by-field assignment (dot notation)
+create type bug19382_foo3 as (a int, b int);
+create function bug19382_test_field_assign() returns record as $$
+declare r bug19382_foo3;
+begin
+    r.a := 123;
+    r.b := power(2, 30)::int4;
+    alter type bug19382_foo3 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_field_assign();
+ERROR:  cannot return record variable "r" after composite type "bug19382_foo3" was altered
+CONTEXT:  PL/pgSQL function bug19382_test_field_assign() line 7 at RETURN
+drop function bug19382_test_field_assign();
+drop type bug19382_foo3 cascade;
+-- Case 6: SELECT INTO individual fields
+create type bug19382_foo4 as (a int, b int);
+create table bug19382_tbl (a int, b int);
+insert into bug19382_tbl values (123, power(2, 30)::int4);
+create function bug19382_test_select_into_field() returns record as $$
+declare r bug19382_foo4;
+begin
+    select a, b into r.a, r.b from bug19382_tbl;
+    alter type bug19382_foo4 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_select_into_field();
+ERROR:  cannot return record variable "r" after composite type "bug19382_foo4" was altered
+CONTEXT:  PL/pgSQL function bug19382_test_select_into_field() line 6 at RETURN
+drop function bug19382_test_select_into_field();
+drop table bug19382_tbl;
+drop type bug19382_foo4 cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..a9c2a0c2233 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,14 @@ exec_move_row(PLpgSQL_execstate *estate,
 				if (rec->erh)
 					DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
 				rec->erh = NULL;
+				/* Clear composite type snapshot */
+				if (rec->compTypeOids)
+					pfree(rec->compTypeOids);
+				if (rec->compTypeVersions)
+					pfree(rec->compTypeVersions);
+				rec->nCompTypes = 0;
+				rec->compTypeOids = NULL;
+				rec->compTypeVersions = NULL;
 			}
 			return;
 		}
@@ -7925,6 +7969,9 @@ instantiate_empty_record_variable(PLpgSQL_execstate *estate, PLpgSQL_rec *rec)
 	/* OK, do it */
 	rec->erh = make_expanded_record_from_typeid(rec->rectypeid, -1,
 												estate->datum_context);
+
+	/* Snapshot composite type versions for ALTER TYPE detection */
+	snapshot_record_composite_types(estate, rec);
 }
 
 /* ----------
@@ -8967,6 +9014,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 +9266,216 @@ 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.
+ *
+ * The outermost type is always checked using er_tupdesc_id (which is set
+ * when the ExpandedRecord is created and works regardless of how the record
+ * was populated, whether by whole assignment, field assignment, etc.).
+ *
+ * Nested composite types are checked against the snapshot taken at record
+ * assignment time, if available.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+	TypeCacheEntry *typentry;
+	Oid			check_typid;
+	int			i;
+
+	/* Nothing to do for anonymous RECORD type */
+	if (rec->rectypeid == RECORDOID)
+		return;
+
+	/*
+	 * Always check outermost type using er_tupdesc_id.  This works for all
+	 * code paths (whole assignment, field assignment, SELECT INTO, etc.)
+	 * because er_tupdesc_id is set when the ExpandedRecord is created.
+	 * Resolve domain types to their base composite type first.
+	 */
+	check_typid = rec->rectypeid;
+	if (get_typtype(check_typid) == TYPTYPE_DOMAIN)
+		check_typid = getBaseType(check_typid);
+
+	typentry = lookup_type_cache(check_typid, TYPECACHE_TUPDESC);
+
+	if (rec->erh->er_tupdesc_id != typentry->tupDesc_identifier)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("cannot return record variable \"%s\" after composite type \"%s\" was altered",
+						rec->refname,
+						format_type_be(check_typid))));
+
+	/*
+	 * If we have a snapshot of nested composite types (taken at whole-record
+	 * assignment time), check those too.  Skip index 0 since that's the
+	 * outermost type we already checked above.
+	 */
+	for (i = 1; i < rec->nCompTypes; i++)
+	{
+		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]))));
+	}
+}
+
+/*
+ * 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;
+
+	/* Resolve domain types to their base composite type */
+	if (get_typtype(typid) == TYPTYPE_DOMAIN)
+		typid = getBaseType(typid);
+
+	/* 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;
+	}
+
+	/*
+	 * Fast path: if we already have a snapshot for this type and the
+	 * outermost type's identifier hasn't changed, the snapshot is still
+	 * valid.  This avoids expensive type tree walks and syscache lookups
+	 * when a record is assigned repeatedly in a loop.
+	 *
+	 * If the identifier HAS changed (ALTER TYPE happened), fall through
+	 * to re-snapshot with the new identifiers.
+	 */
+	if (rec->nCompTypes > 0 && rec->compTypeOids[0] == rec->rectypeid)
+	{
+		TypeCacheEntry *typentry;
+
+		typentry = lookup_type_cache(rec->rectypeid, TYPECACHE_TUPDESC);
+		if (typentry->tupDesc_identifier == rec->compTypeVersions[0])
+			return;		/* type unchanged, snapshot still valid */
+	}
+
+	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..b68fa99258b 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,93 @@ 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;
+
+-- Case 5: Field-by-field assignment (dot notation)
+create type bug19382_foo3 as (a int, b int);
+create function bug19382_test_field_assign() returns record as $$
+declare r bug19382_foo3;
+begin
+    r.a := 123;
+    r.b := power(2, 30)::int4;
+    alter type bug19382_foo3 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_field_assign();
+drop function bug19382_test_field_assign();
+drop type bug19382_foo3 cascade;
+
+-- Case 6: SELECT INTO individual fields
+create type bug19382_foo4 as (a int, b int);
+create table bug19382_tbl (a int, b int);
+insert into bug19382_tbl values (123, power(2, 30)::int4);
+create function bug19382_test_select_into_field() returns record as $$
+declare r bug19382_foo4;
+begin
+    select a, b into r.a, r.b from bug19382_tbl;
+    alter type bug19382_foo4 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_select_into_field();
+drop function bug19382_test_select_into_field();
+drop table bug19382_tbl;
+drop type bug19382_foo4 cascade;
-- 
2.39.5 (Apple Git-154)



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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-17 06:57  Andrey Borodin <[email protected]>
  parent: surya poondla <[email protected]>
  0 siblings, 1 reply; 12+ messages in thread

From: Andrey Borodin @ 2026-04-17 06:57 UTC (permalink / raw)
  To: surya poondla <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>



> On 17 Apr 2026, at 04:20, surya poondla <[email protected]> wrote:
> 
> I also added a fast-path optimization in snapshot_record_composite_types() to avoid repeated type tree walks when a record is assigned in a loop — it skips the snapshot if the outermost type's tupDesc_identifier hasn't changed since the last snapshot.

Cool, but ISTM that it won’t work for domains.

if (rec->nCompTypes > 0 && rec->compTypeOids[0] == rec->rectypeid)

compTypeOids is composite, rectypeid is domain oid.
Can we make this work for domains too?

> Added both new test cases (Cases 5 and 6) to the regression. All 248 core regression tests and all 13 PL/pgSQL tests pass.

I hope it’s the last:

-- Case 7: composite variable used in RAISE NOTICE (exec_eval_datum path).
create type bug19382_foo5 as (a int, b int);
create function bug19382_test_eval_datum() returns void as $$
declare r bug19382_foo5;
begin
r.b := power(2, 30)::int4;
alter type bug19382_foo5 alter attribute b type text;
raise notice 'r = %', r; -- exec_eval_datum called here, no check
end;
$$ language plpgsql;
select bug19382_test_eval_datum();
drop function bug19382_test_eval_datum();
drop type bug19382_foo5 cascade;


Best regards, Andrey Borodin.





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

* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-04-23 04:17  surya poondla <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 0 replies; 12+ messages in thread

From: surya poondla @ 2026-04-23 04:17 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>

Hi Andrey,

Thank you for identifying the domain fast-path bug and RAISE NOTICE. I
fixed both of them in the v7 patch.

Added the test 7 to the suite as well

Thank you again for helping in identifying further bugs and
providing feedback.

Regards,
Surya Poondla


Attachments:

  [application/octet-stream] 0007-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (20.7K, 3-0007-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
  download | inline diff:
From 6e679f302fc136c9a3f716ce51ef8e606ddc4e86 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v7] 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   | 113 +++++++
 src/pl/plpgsql/src/pl_exec.c                  | 286 +++++++++++++++++-
 src/pl/plpgsql/src/plpgsql.h                  |   9 +
 src/pl/plpgsql/src/sql/plpgsql_record.sql     | 104 +++++++
 4 files changed, 511 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..1d2bbeae5a3 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,116 @@ 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
+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
+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
+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;
+-- Case 5: Field-by-field assignment (dot notation)
+create type bug19382_foo3 as (a int, b int);
+create function bug19382_test_field_assign() returns record as $$
+declare r bug19382_foo3;
+begin
+    r.a := 123;
+    r.b := power(2, 30)::int4;
+    alter type bug19382_foo3 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_field_assign();
+ERROR:  cannot return record variable "r" after composite type "bug19382_foo3" was altered
+CONTEXT:  PL/pgSQL function bug19382_test_field_assign() line 7 at RETURN
+drop function bug19382_test_field_assign();
+drop type bug19382_foo3 cascade;
+-- Case 6: SELECT INTO individual fields
+create type bug19382_foo4 as (a int, b int);
+create table bug19382_tbl (a int, b int);
+insert into bug19382_tbl values (123, power(2, 30)::int4);
+create function bug19382_test_select_into_field() returns record as $$
+declare r bug19382_foo4;
+begin
+    select a, b into r.a, r.b from bug19382_tbl;
+    alter type bug19382_foo4 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_select_into_field();
+ERROR:  cannot return record variable "r" after composite type "bug19382_foo4" was altered
+CONTEXT:  PL/pgSQL function bug19382_test_select_into_field() line 6 at RETURN
+drop function bug19382_test_select_into_field();
+drop table bug19382_tbl;
+drop type bug19382_foo4 cascade;
+-- Case 7: RAISE NOTICE with record variable (exec_eval_datum path)
+create type bug19382_foo5 as (a int, b int);
+create function bug19382_test_eval_datum() returns void as $$
+declare r bug19382_foo5;
+begin
+    r.b := power(2, 30)::int4;
+    alter type bug19382_foo5 alter attribute b type text;
+    raise notice 'r = %', r;
+end;
+$$ language plpgsql;
+select bug19382_test_eval_datum();
+ERROR:  cannot return record variable "r" after composite type "bug19382_foo5" was altered
+CONTEXT:  PL/pgSQL function bug19382_test_eval_datum() line 6 at RAISE
+drop function bug19382_test_eval_datum();
+drop type bug19382_foo5 cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..a7c8f8a8bf9 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);
@@ -5451,6 +5487,15 @@ 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.
+					 */
+					if (rec->rectypeid != RECORDOID &&
+						!ExpandedRecordIsEmpty(rec->erh))
+						check_record_type_not_altered(rec);
+
 					if (ExpandedRecordIsEmpty(rec->erh))
 					{
 						/* Empty record is also a NULL */
@@ -7042,6 +7087,14 @@ exec_move_row(PLpgSQL_execstate *estate,
 				if (rec->erh)
 					DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
 				rec->erh = NULL;
+				/* Clear composite type snapshot */
+				if (rec->compTypeOids)
+					pfree(rec->compTypeOids);
+				if (rec->compTypeVersions)
+					pfree(rec->compTypeVersions);
+				rec->nCompTypes = 0;
+				rec->compTypeOids = NULL;
+				rec->compTypeVersions = NULL;
 			}
 			return;
 		}
@@ -7925,6 +7978,9 @@ instantiate_empty_record_variable(PLpgSQL_execstate *estate, PLpgSQL_rec *rec)
 	/* OK, do it */
 	rec->erh = make_expanded_record_from_typeid(rec->rectypeid, -1,
 												estate->datum_context);
+
+	/* Snapshot composite type versions for ALTER TYPE detection */
+	snapshot_record_composite_types(estate, rec);
 }
 
 /* ----------
@@ -8967,6 +9023,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 +9275,228 @@ 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.
+ *
+ * The outermost type is always checked using er_tupdesc_id (which is set
+ * when the ExpandedRecord is created and works regardless of how the record
+ * was populated, whether by whole assignment, field assignment, etc.).
+ *
+ * Nested composite types are checked against the snapshot taken at record
+ * assignment time, if available.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+	TypeCacheEntry *typentry;
+	Oid			check_typid;
+	int			i;
+
+	/* Nothing to do for anonymous RECORD type */
+	if (rec->rectypeid == RECORDOID)
+		return;
+
+	/*
+	 * Always check outermost type using er_tupdesc_id.  This works for all
+	 * code paths (whole assignment, field assignment, SELECT INTO, etc.)
+	 * because er_tupdesc_id is set when the ExpandedRecord is created.
+	 * Resolve domain types to their base composite type first.
+	 */
+	check_typid = rec->rectypeid;
+	if (get_typtype(check_typid) == TYPTYPE_DOMAIN)
+		check_typid = getBaseType(check_typid);
+
+	typentry = lookup_type_cache(check_typid, TYPECACHE_TUPDESC);
+
+	if (rec->erh->er_tupdesc_id != typentry->tupDesc_identifier)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("cannot return record variable \"%s\" after composite type \"%s\" was altered",
+						rec->refname,
+						format_type_be(check_typid))));
+
+	/*
+	 * If we have a snapshot of nested composite types (taken at whole-record
+	 * assignment time), check those too.  Skip index 0 since that's the
+	 * outermost type we already checked above.
+	 */
+	for (i = 1; i < rec->nCompTypes; i++)
+	{
+		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]))));
+	}
+}
+
+/*
+ * 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;
+
+	/* Resolve domain types to their base composite type */
+	if (get_typtype(typid) == TYPTYPE_DOMAIN)
+		typid = getBaseType(typid);
+
+	/* 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;
+	}
+
+	/*
+	 * Fast path: if we already have a snapshot for this type and the
+	 * outermost type's identifier hasn't changed, the snapshot is still
+	 * valid.  This avoids expensive type tree walks and syscache lookups
+	 * when a record is assigned repeatedly in a loop.
+	 *
+	 * If the identifier HAS changed (ALTER TYPE happened), fall through
+	 * to re-snapshot with the new identifiers.
+	 *
+	 * Note: compTypeOids[0] stores the resolved base composite type OID
+	 * (domains are resolved by collect_composite_type_versions), so we
+	 * must resolve rec->rectypeid before comparing.
+	 */
+	if (rec->nCompTypes > 0)
+	{
+		Oid			root_typid = rec->rectypeid;
+
+		if (get_typtype(root_typid) == TYPTYPE_DOMAIN)
+			root_typid = getBaseType(root_typid);
+
+		if (rec->compTypeOids[0] == root_typid)
+		{
+			TypeCacheEntry *typentry;
+
+			typentry = lookup_type_cache(root_typid, TYPECACHE_TUPDESC);
+			if (typentry->tupDesc_identifier == rec->compTypeVersions[0])
+				return;		/* type unchanged, snapshot still valid */
+		}
+	}
+
+	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..f4035881c86 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,107 @@ 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;
+
+-- Case 5: Field-by-field assignment (dot notation)
+create type bug19382_foo3 as (a int, b int);
+create function bug19382_test_field_assign() returns record as $$
+declare r bug19382_foo3;
+begin
+    r.a := 123;
+    r.b := power(2, 30)::int4;
+    alter type bug19382_foo3 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_field_assign();
+drop function bug19382_test_field_assign();
+drop type bug19382_foo3 cascade;
+
+-- Case 6: SELECT INTO individual fields
+create type bug19382_foo4 as (a int, b int);
+create table bug19382_tbl (a int, b int);
+insert into bug19382_tbl values (123, power(2, 30)::int4);
+create function bug19382_test_select_into_field() returns record as $$
+declare r bug19382_foo4;
+begin
+    select a, b into r.a, r.b from bug19382_tbl;
+    alter type bug19382_foo4 alter attribute b type text;
+    return r;
+end;
+$$ language plpgsql;
+select bug19382_test_select_into_field();
+drop function bug19382_test_select_into_field();
+drop table bug19382_tbl;
+drop type bug19382_foo4 cascade;
+
+-- Case 7: RAISE NOTICE with record variable (exec_eval_datum path)
+create type bug19382_foo5 as (a int, b int);
+create function bug19382_test_eval_datum() returns void as $$
+declare r bug19382_foo5;
+begin
+    r.b := power(2, 30)::int4;
+    alter type bug19382_foo5 alter attribute b type text;
+    raise notice 'r = %', r;
+end;
+$$ language plpgsql;
+select bug19382_test_eval_datum();
+drop function bug19382_test_eval_datum();
+drop type bug19382_foo5 cascade;
-- 
2.39.5 (Apple Git-154)



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


end of thread, other threads:[~2026-04-23 04:17 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-03-19 05:00 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-03-19 07:53 ` =?utf-8?B?c29uZ2ppbnpob3U=?= <[email protected]>
2026-03-20 18:16   ` surya poondla <[email protected]>
2026-04-02 11:18     ` Andrey Borodin <[email protected]>
2026-04-02 23:14       ` surya poondla <[email protected]>
2026-04-04 12:42         ` Andrey Borodin <[email protected]>
2026-04-09 04:24           ` surya poondla <[email protected]>
2026-04-14 23:49             ` surya poondla <[email protected]>
2026-04-16 12:00             ` Andrey Borodin <[email protected]>
2026-04-16 23:20               ` surya poondla <[email protected]>
2026-04-17 06:57                 ` Andrey Borodin <[email protected]>
2026-04-23 04:17                   ` surya poondla <[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