public inbox for [email protected]
help / color / mirror / Atom feedRe: BUG #19382: Server crash at __nss_database_lookup
16+ messages / 4 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; 16+ 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, ../../CAOVWO5oSeBouPv0ueVByh+_6EgRCjWh0spSmnF6Cv-TF1twqKg@mail.gmail.com/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] 16+ 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; 16+ 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]>
发件时间:2026年3月19日 13:00
收件人:dllggyx <[email protected]>, pgsql-bugs <[email protected]>
主题: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: 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
^ permalink raw reply [nested|flat] 16+ 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; 16+ 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] 16+ 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; 16+ 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] 16+ 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; 16+ 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, ../../CAOVWO5oRGPd7mA3d85jNYmjLNfeBAca5oDcHTfRFxbAwPLxs5g@mail.gmail.com/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] 16+ 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; 16+ 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, ../../[email protected]/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] 16+ 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; 16+ 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, ../../CAOVWO5o9YOpCTgg6FfNepCoH_6pFSa7TJ3SEWfJAoBvNOb0OdQ@mail.gmail.com/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] 16+ 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; 16+ 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, ../../CAOVWO5oMi69pO+sAE4cWU0xjNCXarP=UpaZVQqLyQnBmdjT8uw@mail.gmail.com/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] 16+ 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; 16+ 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] 16+ 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; 16+ 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, ../../CAOVWO5pjr=qTkf0fMFfrhtnweBJihxkm=NhhuNuoBfrTAgP5ew@mail.gmail.com/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] 16+ 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; 16+ 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] 16+ 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, 1 reply; 16+ 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, ../../CAOVWO5r3-yzw=Baamiu-reus8H3tRwxsVMp7cmQmqx_f2+Lo6g@mail.gmail.com/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] 16+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-06-02 23:44 surya poondla <[email protected]>
parent: surya poondla <[email protected]>
0 siblings, 1 reply; 16+ messages in thread
From: surya poondla @ 2026-06-02 23:44 UTC (permalink / raw)
To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>
Hi All,
Rebased the patch to the latest code.
Regards,
Surya Poondla
>
Attachments:
[application/octet-stream] 0008-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (20.7K, ../../CAOVWO5q0X+fCjb9MSBwR-q6EEM7M_KKEkBvy1kWwuMvX4dyHiQ@mail.gmail.com/3-0008-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
download | inline diff:
From 3668132edc284cd882299d67dea884dbd37c1d4a Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v8] 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] 16+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-06-30 20:50 surya poondla <[email protected]>
parent: surya poondla <[email protected]>
0 siblings, 2 replies; 16+ messages in thread
From: surya poondla @ 2026-06-30 20:50 UTC (permalink / raw)
To: Andrey Borodin <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>
Hi All,
Rebased the patch to the latest code.
Regards,
Surya Poondla
Attachments:
[application/octet-stream] 0009-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (20.7K, ../../CAOVWO5p5vUZVeRVTANoHY-CpD1L5szk96aKX5hDW-woyOH7DVA@mail.gmail.com/3-0009-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
download | inline diff:
From 1c5af79cfd0b055b36f10adc2201bf26b2c6d55e Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v9] 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] 16+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-07-06 22:16 Zsolt Parragi <[email protected]>
parent: surya poondla <[email protected]>
1 sibling, 0 replies; 16+ messages in thread
From: Zsolt Parragi @ 2026-07-06 22:16 UTC (permalink / raw)
To: [email protected]
Hello!
The following still reproduces the crash with the patch:
create type foo as (a int, b int);
create function bar_rec() returns record as $$
declare r record;
begin
r := row(123, power(2,30)::int4)::foo;
alter type foo alter attribute b type text;
return r;
end $$ language plpgsql;
select bar_rec();
And also, shouldn't the following still work?
create type inn2 as (x int, y int);
create type out2 as (a int, b inn2);
create function fp_test() returns out2 as $$
declare r out2;
begin
r := row(1, row(10, 20)::inn2)::out2;
alter type inn2 alter attribute y type text;
r := row(1, row(10, 'hello')::inn2)::out2;
return r;
end $$ language plpgsql;
select fp_test();
-- ERROR: cannot return record variable "r" after composite type
"inn2" was altered
but it only uses out2.
^ permalink raw reply [nested|flat] 16+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-07-07 23:20 surya poondla <[email protected]>
parent: surya poondla <[email protected]>
1 sibling, 0 replies; 16+ messages in thread
From: surya poondla @ 2026-07-07 23:20 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: songjinzhou <[email protected]>; dllggyx <[email protected]>; pgsql-bugs <[email protected]>; Andrey Borodin <[email protected]>
Hi Zsolt,
Thanks for the testing, both issues you reported were real; v10 (attached)
fixes them.
Case 1 (plain "record" still crashed): the snapshot keyed off the
variable's declared type, which is just RECORDOID for a RECORD variable, so
nothing was tracked.
v10 uses the actual composite type adopted from the assigned value
(er_typeid), so "r record := ROW(...)::foo" is now covered.
Case 2 (reassignment wrongly rejected): a fast path only re-checked the
outermost type, so a stale nested entry survived a reassignment and caused
a false positive.
v10 refreshes the snapshot on every whole-record assignment, so reassigning
with fresh data after an ALTER now succeeds (matching master), while the
same sequence without the reassignment still errors.
While testing, I also found a related crash the earlier versions missed: a
composite reached through a container (e.g. a field of type "t_comp[]")
wasn't tracked, because the recursion stopped at the array. An ALTER TYPE
on the element type then crashed in record_out() and caused a connection
timeout.
v10 iterates domain/array/range/multirange layers to reach the composite
element.
v10 adds regression coverage for all of the above cases.
Attachments:
[application/octet-stream] v10-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (34.9K, ../../CAOVWO5pjSeYh1UOGaROXr51GhAf9QsLXJeqp__-D4BtHs5oESA@mail.gmail.com/3-v10-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
download | inline diff:
From 8872b44f35dc4f7b6ecb0f77e034500fec5f0fa3 Mon Sep 17 00:00:00 2001
From: spoondla <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v10] 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. Reading such a record later
(RETURN, RETURN NEXT, RAISE, etc.) crashes because the output functions
interpret the stored bytes using the new type definition. The crash
manifested as a segmentation fault in record_out() when it attempted to
interpret integer data as a text pointer.
The fix snapshots tupDesc_identifier values for all composite types
reachable from a record variable's type at assignment time, and compares
them against the current type cache values before the record is read. If
any identifier has changed, an error is raised instead of risking a crash.
Details:
1. The type tree is walked from an effective root type. For a variable
declared as a named composite the declared type is used; for a variable
declared as generic RECORD the actual type adopted from the assigned
value (er_typeid) is used, so "r record := ROW(...)::foo" is covered.
Truly anonymous rowtypes are not versioned and are left unchecked.
2. The walk descends through domains and through container types (arrays,
ranges, and multiranges) to reach composite element types, so a field
such as "t_comp[]" is tracked and an ALTER TYPE on t_comp is detected.
3. The snapshot is refreshed on every whole-record assignment, including
the in-place expanded_record_set_tuple paths, so that reassigning a
record with fresh data after an ALTER TYPE is correctly allowed rather
than rejected.
The outermost type is additionally checked via the ExpandedRecord's
er_tupdesc_id, which covers field-by-field assignment and SELECT INTO
paths that do not rebuild the record wholesale.
---
.../plpgsql/src/expected/plpgsql_record.out | 262 ++++++++++++++
src/pl/plpgsql/src/pl_exec.c | 324 +++++++++++++++++-
src/pl/plpgsql/src/plpgsql.h | 9 +
src/pl/plpgsql/src/sql/plpgsql_record.sql | 236 +++++++++++++
4 files changed, 830 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..38aa162bdea 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,265 @@ 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;
+-- Case 8: Generic RECORD variable assigned via ROW::foo cast
+-- The declared type is RECORDOID; effective type is discovered from the
+-- assigned value's er_typeid. Must error, not crash.
+create type bug19382_foo6 as (a int, b int);
+create function bug19382_test_generic_record() returns record as $$
+declare r record;
+begin
+ r := row(123, power(2, 30)::int4)::bug19382_foo6;
+ alter type bug19382_foo6 alter attribute b type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_generic_record();
+ERROR: cannot return record variable "r" after composite type "bug19382_foo6" was altered
+CONTEXT: PL/pgSQL function bug19382_test_generic_record() line 6 at RETURN
+drop function bug19382_test_generic_record();
+drop type bug19382_foo6 cascade;
+-- Case 9: Generic RECORD with nested composite alter
+create type bug19382_inner2 as (x int, y int);
+create type bug19382_outer2 as (a int, b bug19382_inner2);
+create function bug19382_test_generic_nested() returns record as $$
+declare r record;
+begin
+ r := row(1, row(10, power(2, 30)::int4)::bug19382_inner2)::bug19382_outer2;
+ alter type bug19382_inner2 alter attribute y type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_generic_nested();
+ERROR: cannot return record variable "r" after composite type "bug19382_inner2" was altered
+CONTEXT: PL/pgSQL function bug19382_test_generic_nested() line 6 at RETURN
+drop function bug19382_test_generic_nested();
+drop type bug19382_outer2 cascade;
+drop type bug19382_inner2 cascade;
+-- Case 10: Whole-record reassignment after inner-type alter.
+-- The second assignment builds fresh data matching the post-ALTER type;
+-- the snapshot must refresh so the reassignment succeeds.
+create type bug19382_inner3 as (x int, y int);
+create type bug19382_outer3 as (a int, b bug19382_inner3);
+create function bug19382_test_reassign() returns bug19382_outer3 as $$
+declare r bug19382_outer3;
+begin
+ r := row(1, row(10, 20)::bug19382_inner3)::bug19382_outer3;
+ alter type bug19382_inner3 alter attribute y type text;
+ r := row(1, row(10, 'hello')::bug19382_inner3)::bug19382_outer3;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_reassign();
+ bug19382_test_reassign
+------------------------
+ (1,"(10,hello)")
+(1 row)
+
+drop function bug19382_test_reassign();
+drop type bug19382_outer3 cascade;
+drop type bug19382_inner3 cascade;
+-- Case 11: Anonymous rowtype baseline (RECORDOID with no ::foo cast).
+-- Non-versionable rowtypes must not trigger a false positive.
+create function bug19382_test_anon_baseline() returns record as $$
+declare r record;
+begin
+ select 1 as a, 2 as b into r;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_anon_baseline();
+ bug19382_test_anon_baseline
+-----------------------------
+ (1,2)
+(1 row)
+
+drop function bug19382_test_anon_baseline();
+-- Case 12: Variable-to-variable copy after inner-type alter.
+-- Reading r1 as an rvalue must detect the stale nested type.
+create type bug19382_inner4 as (x int, y int);
+create type bug19382_outer4 as (a int, b bug19382_inner4);
+create function bug19382_test_var_copy() returns bug19382_outer4 as $$
+declare r1 bug19382_outer4; r2 bug19382_outer4;
+begin
+ r1 := row(1, row(10, power(2, 30)::int4)::bug19382_inner4)::bug19382_outer4;
+ alter type bug19382_inner4 alter attribute y type text;
+ r2 := r1;
+ return r2;
+end;
+$$ language plpgsql;
+select bug19382_test_var_copy();
+ERROR: cannot return record variable "r1" after composite type "bug19382_inner4" was altered
+CONTEXT: PL/pgSQL function bug19382_test_var_copy() line 6 at assignment
+drop function bug19382_test_var_copy();
+drop type bug19382_outer4 cascade;
+drop type bug19382_inner4 cascade;
+-- Case 13: RAISE NOTICE with generic RECORD (RECORDOID + reader path).
+create type bug19382_foo7 as (a int, b int);
+create function bug19382_test_generic_raise() returns void as $$
+declare r record;
+begin
+ r := row(1, power(2, 30)::int4)::bug19382_foo7;
+ alter type bug19382_foo7 alter attribute b type text;
+ raise notice 'r = %', r;
+end;
+$$ language plpgsql;
+select bug19382_test_generic_raise();
+ERROR: cannot return record variable "r" after composite type "bug19382_foo7" was altered
+CONTEXT: PL/pgSQL function bug19382_test_generic_raise() line 6 at RAISE
+drop function bug19382_test_generic_raise();
+drop type bug19382_foo7 cascade;
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+ r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+ alter type bug19382_elem alter attribute f1 type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+ERROR: cannot return record variable "r" after composite type "bug19382_elem" was altered
+CONTEXT: PL/pgSQL function bug19382_test_array_elem() line 6 at RETURN
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+ r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ alter type bug19382_elem2 add attribute f2 text;
+ r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+ bug19382_test_array_reassign
+------------------------------
+ ("{""(1,hi)""}")
+(1 row)
+
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..9c0be5ac756 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->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->erh != NULL && !ExpandedRecordIsEmpty(rec->erh))
+ 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,14 @@ 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 (!ExpandedRecordIsEmpty(rec->erh))
+ check_record_type_not_altered(rec);
+
if (ExpandedRecordIsEmpty(rec->erh))
{
/* Empty record is also a NULL */
@@ -7042,6 +7086,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;
}
@@ -7745,6 +7797,13 @@ exec_move_row_from_datum(PLpgSQL_execstate *estate,
{
expanded_record_set_tuple(rec->erh, erh->fvalue,
true, !estate->atomic);
+ /*
+ * The tuple bytes were replaced in place. Refresh the
+ * composite-type snapshot so a later ALTER TYPE on any
+ * reachable type is detected, and stale snapshot entries
+ * from a prior assignment are not carried forward.
+ */
+ snapshot_record_composite_types(estate, rec);
return;
}
@@ -7859,6 +7918,8 @@ exec_move_row_from_datum(PLpgSQL_execstate *estate,
{
expanded_record_set_tuple(rec->erh, &tmptup,
true, !estate->atomic);
+ /* See comment in the analogous branch above. */
+ snapshot_record_composite_types(estate, rec);
return;
}
@@ -7925,6 +7986,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 +9031,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 +9283,258 @@ 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;
+
+ /*
+ * Determine the effective type to check. For a variable declared as
+ * generic RECORD, use the ExpandedRecord's actual type once it's been
+ * assigned. If that too is RECORDOID (truly anonymous rowtype), there
+ * is nothing to check because such rowtypes are not versioned.
+ */
+ if (rec->rectypeid != RECORDOID)
+ check_typid = rec->rectypeid;
+ else if (rec->erh != NULL && rec->erh->er_typeid != RECORDOID)
+ check_typid = rec->erh->er_typeid;
+ else
+ 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.
+ */
+ 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;
+
+ if (attr->attisdropped)
+ continue;
+
+ attrtypid = attr->atttypid;
+
+ /*
+ * A record field can reference a composite type directly, or wrap
+ * one inside a domain or a container type (array, range, or
+ * multirange). ALTER TYPE on such an indirectly-referenced
+ * composite must still be detected, so peel off domain and
+ * container layers until we reach a composite type (which we
+ * snapshot) or something that cannot contain one (which we skip).
+ * This terminates because each peel step yields a strictly
+ * "smaller" type. Container element types cannot be self-referential.
+ */
+ for (;;)
+ {
+ char typtype = get_typtype(attrtypid);
+ Oid elemtypid;
+
+ if (attrtypid == RECORDOID)
+ break; /* anonymous rowtype: not versionable */
+
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ attrtypid = getBaseType(attrtypid);
+ continue;
+ }
+
+ if (typtype == TYPTYPE_COMPOSITE)
+ {
+ collect_composite_type_versions(attrtypid,
+ oids, versions, n, alloc);
+ break;
+ }
+
+ /* Array element type? */
+ elemtypid = get_element_type(attrtypid);
+
+ /* Otherwise a range's subtype? */
+ if (!OidIsValid(elemtypid))
+ elemtypid = get_range_subtype(attrtypid);
+
+ /* Otherwise a multirange's range's subtype? */
+ if (!OidIsValid(elemtypid) && typtype == TYPTYPE_MULTIRANGE)
+ {
+ Oid rangetypid = get_multirange_range(attrtypid);
+
+ if (OidIsValid(rangetypid))
+ elemtypid = get_range_subtype(rangetypid);
+ }
+
+ if (!OidIsValid(elemtypid))
+ break; /* not a composite-bearing type */
+
+ attrtypid = elemtypid;
+ }
+ }
+}
+
+/*
+ * 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.
+ *
+ * For a variable declared as generic RECORD (rectypeid == RECORDOID), the
+ * effective root type is taken from the ExpandedRecord's er_typeid, which
+ * reflects the actual composite type adopted from the assigned value. If
+ * that too is RECORDOID (truly anonymous rowtype), no snapshot is taken
+ * because such rowtypes are not versioned by the type cache.
+ */
+static void
+snapshot_record_composite_types(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec)
+{
+ MemoryContext oldcxt;
+ Oid root_typid;
+ int alloc = 8;
+ int n = 0;
+ Oid *oids;
+ uint64 *versions;
+
+ /*
+ * Determine the effective root type. For a variable declared as generic
+ * RECORD, use the ExpandedRecord's actual type once it's been assigned.
+ * Truly anonymous rowtypes (er_typeid still RECORDOID) cannot be tracked.
+ */
+ if (rec->rectypeid != RECORDOID)
+ root_typid = rec->rectypeid;
+ else if (rec->erh != NULL && rec->erh->er_typeid != RECORDOID)
+ root_typid = rec->erh->er_typeid;
+ else
+ {
+ rec->nCompTypes = 0;
+ return;
+ }
+
+ oldcxt = MemoryContextSwitchTo(estate->datum_context);
+ oids = palloc(alloc * sizeof(Oid));
+ versions = palloc(alloc * sizeof(uint64));
+
+ collect_composite_type_versions(root_typid,
+ &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..cc1406a663d 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,239 @@ 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;
+
+-- Case 8: Generic RECORD variable assigned via ROW::foo cast
+-- The declared type is RECORDOID; effective type is discovered from the
+-- assigned value's er_typeid. Must error, not crash.
+create type bug19382_foo6 as (a int, b int);
+create function bug19382_test_generic_record() returns record as $$
+declare r record;
+begin
+ r := row(123, power(2, 30)::int4)::bug19382_foo6;
+ alter type bug19382_foo6 alter attribute b type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_generic_record();
+drop function bug19382_test_generic_record();
+drop type bug19382_foo6 cascade;
+
+-- Case 9: Generic RECORD with nested composite alter
+create type bug19382_inner2 as (x int, y int);
+create type bug19382_outer2 as (a int, b bug19382_inner2);
+create function bug19382_test_generic_nested() returns record as $$
+declare r record;
+begin
+ r := row(1, row(10, power(2, 30)::int4)::bug19382_inner2)::bug19382_outer2;
+ alter type bug19382_inner2 alter attribute y type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_generic_nested();
+drop function bug19382_test_generic_nested();
+drop type bug19382_outer2 cascade;
+drop type bug19382_inner2 cascade;
+
+-- Case 10: Whole-record reassignment after inner-type alter.
+-- The second assignment builds fresh data matching the post-ALTER type;
+-- the snapshot must refresh so the reassignment succeeds.
+create type bug19382_inner3 as (x int, y int);
+create type bug19382_outer3 as (a int, b bug19382_inner3);
+create function bug19382_test_reassign() returns bug19382_outer3 as $$
+declare r bug19382_outer3;
+begin
+ r := row(1, row(10, 20)::bug19382_inner3)::bug19382_outer3;
+ alter type bug19382_inner3 alter attribute y type text;
+ r := row(1, row(10, 'hello')::bug19382_inner3)::bug19382_outer3;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_reassign();
+drop function bug19382_test_reassign();
+drop type bug19382_outer3 cascade;
+drop type bug19382_inner3 cascade;
+
+-- Case 11: Anonymous rowtype baseline (RECORDOID with no ::foo cast).
+-- Non-versionable rowtypes must not trigger a false positive.
+create function bug19382_test_anon_baseline() returns record as $$
+declare r record;
+begin
+ select 1 as a, 2 as b into r;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_anon_baseline();
+drop function bug19382_test_anon_baseline();
+
+-- Case 12: Variable-to-variable copy after inner-type alter.
+-- Reading r1 as an rvalue must detect the stale nested type.
+create type bug19382_inner4 as (x int, y int);
+create type bug19382_outer4 as (a int, b bug19382_inner4);
+create function bug19382_test_var_copy() returns bug19382_outer4 as $$
+declare r1 bug19382_outer4; r2 bug19382_outer4;
+begin
+ r1 := row(1, row(10, power(2, 30)::int4)::bug19382_inner4)::bug19382_outer4;
+ alter type bug19382_inner4 alter attribute y type text;
+ r2 := r1;
+ return r2;
+end;
+$$ language plpgsql;
+select bug19382_test_var_copy();
+drop function bug19382_test_var_copy();
+drop type bug19382_outer4 cascade;
+drop type bug19382_inner4 cascade;
+
+-- Case 13: RAISE NOTICE with generic RECORD (RECORDOID + reader path).
+create type bug19382_foo7 as (a int, b int);
+create function bug19382_test_generic_raise() returns void as $$
+declare r record;
+begin
+ r := row(1, power(2, 30)::int4)::bug19382_foo7;
+ alter type bug19382_foo7 alter attribute b type text;
+ raise notice 'r = %', r;
+end;
+$$ language plpgsql;
+select bug19382_test_generic_raise();
+drop function bug19382_test_generic_raise();
+drop type bug19382_foo7 cascade;
+
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+ r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+ alter type bug19382_elem alter attribute f1 type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+ r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ alter type bug19382_elem2 add attribute f2 text;
+ r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 16+ messages in thread
end of thread, other threads:[~2026-07-07 23:20 UTC | newest]
Thread overview: 16+ messages (download: mbox 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]>
2026-06-02 23:44 ` surya poondla <[email protected]>
2026-06-30 20:50 ` surya poondla <[email protected]>
2026-07-06 22:16 ` Zsolt Parragi <[email protected]>
2026-07-07 23:20 ` 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