agora inbox for pgsql-bugs@postgresql.org
help / color / mirror / Atom feedBUG #19382: Server crash at __nss_database_lookup
30+ messages / 6 participants
[nested] [flat]
* BUG #19382: Server crash at __nss_database_lookup
@ 2026-01-20 05:02 PG Bug reporting form <noreply@postgresql.org>
2026-01-20 09:15 ` Re: BUG #19382: Server crash at __nss_database_lookup Kirill Reshke <reshkekirill@gmail.com>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 2 replies; 30+ messages in thread
From: PG Bug reporting form @ 2026-01-20 05:02 UTC (permalink / raw)
To: pgsql-bugs@lists.postgresql.org; +Cc: dllggyx@outlook.com
The following bug has been logged on the website:
Bug reference: 19382
Logged by: Yuxiao Guo
Email address: dllggyx@outlook.com
PostgreSQL version: 17.7
Operating system: Ubuntu 20.04 x86-64, docker image postgres:17.7
Description:
Hi, I found a crash in PostgreSQL. Here are the details:
PoC:
DROP FUNCTION IF EXISTS bar();
DROP TYPE IF EXISTS foo CASCADE;
CREATE TYPE foo AS (a INT, b INT);
CREATE FUNCTION bar() RETURNS RECORD AS $$
DECLARE
r foo := ROW(123, power(2, 30));
BEGIN
ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
RETURN r;
END;
$$ LANGUAGE plpgsql;
SELECT bar();
DROP TYPE IF EXISTS foo CASCADE;
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;
Stacktrace:
#0 0x7182a7813b38 (__nss_database_lookup+0x284b8)
#1 0x91c9e5 (textout+0x85)
#2 0x94cbd3 (OutputFunctionCall+0x33)
#3 0x8db524 (record_out+0x244)
#4 0x94cbd3 (OutputFunctionCall+0x33)
#5 0x49fafc (printtup+0x2bc)
#6 0x7fc888 (RunFromStore+0xa8)
#7 0x7fbeb5 (PortalRunSelect+0x75)
#8 0x7fbbbe (PortalRun+0x16e)
#9 0x7fadd5 (exec_simple_query+0x585)
#10 0x7f8929 (PostgresMain+0x949)
#11 0x7f45bf (BackendMain+0x3f)
#12 0x75df24 (postmaster_child_launch+0x94)
#13 0x762101 (ServerLoop+0x1d61)
#14 0x75faab (PostmasterMain+0xd8b)
#15 0x6a3349 (main+0x2f9)
#16 0x7182a76ac083 (__libc_start_main+0xf3)
#17 0x49006e (_start+0x2e)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
@ 2026-01-20 09:15 ` Kirill Reshke <reshkekirill@gmail.com>
2026-01-20 09:25 ` Re: BUG #19382: Server crash at __nss_database_lookup Kirill Reshke <reshkekirill@gmail.com>
1 sibling, 1 reply; 30+ messages in thread
From: Kirill Reshke @ 2026-01-20 09:15 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
On Tue, 20 Jan 2026 at 13:58, PG Bug reporting form
<noreply@postgresql.org> wrote:
>
> The following bug has been logged on the website:
>
> Bug reference: 19382
> Logged by: Yuxiao Guo
> Email address: dllggyx@outlook.com
> PostgreSQL version: 17.7
> Operating system: Ubuntu 20.04 x86-64, docker image postgres:17.7
> Description:
>
> Hi, I found a crash in PostgreSQL. Here are the details:
>
> PoC:
> DROP FUNCTION IF EXISTS bar();
> DROP TYPE IF EXISTS foo CASCADE;
> CREATE TYPE foo AS (a INT, b INT);
> CREATE FUNCTION bar() RETURNS RECORD AS $$
> DECLARE
> r foo := ROW(123, power(2, 30));
> BEGIN
> ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
> RETURN r;
> END;
> $$ LANGUAGE plpgsql;
> SELECT bar();
>
> DROP TYPE IF EXISTS foo CASCADE;
> 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;
>
>
> Stacktrace:
> #0 0x7182a7813b38 (__nss_database_lookup+0x284b8)
> #1 0x91c9e5 (textout+0x85)
> #2 0x94cbd3 (OutputFunctionCall+0x33)
> #3 0x8db524 (record_out+0x244)
> #4 0x94cbd3 (OutputFunctionCall+0x33)
> #5 0x49fafc (printtup+0x2bc)
> #6 0x7fc888 (RunFromStore+0xa8)
> #7 0x7fbeb5 (PortalRunSelect+0x75)
> #8 0x7fbbbe (PortalRun+0x16e)
> #9 0x7fadd5 (exec_simple_query+0x585)
> #10 0x7f8929 (PostgresMain+0x949)
> #11 0x7f45bf (BackendMain+0x3f)
> #12 0x75df24 (postmaster_child_launch+0x94)
> #13 0x762101 (ServerLoop+0x1d61)
> #14 0x75faab (PostmasterMain+0xd8b)
> #15 0x6a3349 (main+0x2f9)
> #16 0x7182a76ac083 (__libc_start_main+0xf3)
> #17 0x49006e (_start+0x2e)
>
>
>
>
reproduced here on HEAD
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-20 09:15 ` Re: BUG #19382: Server crash at __nss_database_lookup Kirill Reshke <reshkekirill@gmail.com>
@ 2026-01-20 09:25 ` Kirill Reshke <reshkekirill@gmail.com>
2026-01-20 09:47 ` Re: BUG #19382: Server crash at __nss_database_lookup Kirill Reshke <reshkekirill@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: Kirill Reshke @ 2026-01-20 09:25 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
On Tue, 20 Jan 2026 at 14:15, Kirill Reshke <reshkekirill@gmail.com> wrote:
>
> On Tue, 20 Jan 2026 at 13:58, PG Bug reporting form
> <noreply@postgresql.org> wrote:
> >
> > The following bug has been logged on the website:
> >
> > Bug reference: 19382
> > Logged by: Yuxiao Guo
> > Email address: dllggyx@outlook.com
> > PostgreSQL version: 17.7
> > Operating system: Ubuntu 20.04 x86-64, docker image postgres:17.7
> > Description:
> >
> > Hi, I found a crash in PostgreSQL. Here are the details:
> >
> > PoC:
> > DROP FUNCTION IF EXISTS bar();
> > DROP TYPE IF EXISTS foo CASCADE;
> > CREATE TYPE foo AS (a INT, b INT);
> > CREATE FUNCTION bar() RETURNS RECORD AS $$
> > DECLARE
> > r foo := ROW(123, power(2, 30));
> > BEGIN
> > ALTER TYPE foo ALTER ATTRIBUTE b TYPE TEXT;
> > RETURN r;
> > END;
> > $$ LANGUAGE plpgsql;
> > SELECT bar();
> >
> > DROP TYPE IF EXISTS foo CASCADE;
> > 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;
> >
> >
> > Stacktrace:
> > #0 0x7182a7813b38 (__nss_database_lookup+0x284b8)
> > #1 0x91c9e5 (textout+0x85)
> > #2 0x94cbd3 (OutputFunctionCall+0x33)
> > #3 0x8db524 (record_out+0x244)
> > #4 0x94cbd3 (OutputFunctionCall+0x33)
> > #5 0x49fafc (printtup+0x2bc)
> > #6 0x7fc888 (RunFromStore+0xa8)
> > #7 0x7fbeb5 (PortalRunSelect+0x75)
> > #8 0x7fbbbe (PortalRun+0x16e)
> > #9 0x7fadd5 (exec_simple_query+0x585)
> > #10 0x7f8929 (PostgresMain+0x949)
> > #11 0x7f45bf (BackendMain+0x3f)
> > #12 0x75df24 (postmaster_child_launch+0x94)
> > #13 0x762101 (ServerLoop+0x1d61)
> > #14 0x75faab (PostmasterMain+0xd8b)
> > #15 0x6a3349 (main+0x2f9)
> > #16 0x7182a76ac083 (__libc_start_main+0xf3)
> > #17 0x49006e (_start+0x2e)
> >
> >
> >
> >
>
> reproduced here on HEAD
>
In fact, this fails as fast as 'SELECT bar()' for me. Also, it fails
for REL_14_STABLE, REL_16_STABLE, so, problem is for all supported
versions
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-20 09:15 ` Re: BUG #19382: Server crash at __nss_database_lookup Kirill Reshke <reshkekirill@gmail.com>
2026-01-20 09:25 ` Re: BUG #19382: Server crash at __nss_database_lookup Kirill Reshke <reshkekirill@gmail.com>
@ 2026-01-20 09:47 ` Kirill Reshke <reshkekirill@gmail.com>
0 siblings, 0 replies; 30+ messages in thread
From: Kirill Reshke @ 2026-01-20 09:47 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
> In fact, this fails as fast as 'SELECT bar()' for me. Also, it fails
> for REL_14_STABLE, REL_16_STABLE, so, problem is for all supported
> versions
Below fails in same way:
"
CREATE TABLE foo (a INT, b INT);
CREATE FUNCTION bar() RETURNS RECORD AS $$
DECLARE
r foo := ROW(1, 1);
BEGIN
ALTER table foo alter column b type text;
RETURN r;
END;
$$ LANGUAGE plpgsql;
SELECT bar();
"
My current idea is that we should somehow reject the ALTER type if it
is used to declare part of a function. Maybe exec_assign_expr should
report all types that are used to evaluate expressions in the declare
part?
--
Best regards,
Kirill Reshke
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
@ 2026-01-23 01:18 ` surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
1 sibling, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-01-23 01:18 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
Hi Yuxiao, Kirill,
Thank you for the test cases.
I can reproduce this issue on PostgreSQL 17.6. I debugged it with lldb and
found the root cause.
When a composite type is altered mid-transaction while a PL/pgSQL record
variable holds data of that type, the server crashes because it interprets
old data using the new type definition without performing type conversion.
The server crashes with this stack trace:
* thread #1, queue = 'main-thread', stop reason = EXC_BAD_ACCESS (code=1,
address=0x117e00000)
frame #0: 0x0000000183c95320 libsystem_platform.dylib`_platform_memmove
+ 96
libsystem_platform.dylib`_platform_memmove:
-> 0x183c95320 <+96>: ldnp q0, q1, [x1]
0x183c95324 <+100>: add x1, x1, #0x20
0x183c95328 <+104>: subs x2, x2, #0x20
0x183c9532c <+108>: b.hi 0x183c95318 ; <+88>
Target 0: (postgres) stopped.
(lldb) bt
* thread #1, queue = 'main-thread', stop reason = EXC_BAD_ACCESS (code=1,
address=0x117e00000)
* frame #0: 0x0000000183c95320 libsystem_platform.dylib`_platform_memmove
+ 96
frame #1: 0x00000001030ef368
postgres`text_to_cstring(t=0x0000000117017b1c) at varlena.c:225:2
frame #2: 0x00000001030f0e58
postgres`textout(fcinfo=0x000000016d5f1b98) at varlena.c:594:2
frame #3: 0x000000010314ed14
postgres`FunctionCall1Coll(flinfo=0x0000000121808cd8, collation=0,
arg1=4680940316) at fmgr.c:1139:11
frame #4: 0x0000000103150880
postgres`OutputFunctionCall(flinfo=0x0000000121808cd8, val=4680940316) at
fmgr.c:1685:25
frame #5: 0x0000000103075c8c
postgres`record_out(fcinfo=0x000000016d5f1d58) at rowtypes.c:435:11
frame #6: 0x000000010314ed14
postgres`FunctionCall1Coll(flinfo=0x0000000121808a28, collation=0,
arg1=4940960546) at fmgr.c:1139:11
frame #7: 0x0000000103150880
postgres`OutputFunctionCall(flinfo=0x0000000121808a28, val=4940960546) at
fmgr.c:1685:25
frame #8: 0x000000010282fa30 postgres`printtup(slot=0x00000001218087a8,
self=0x00000001170102d8) at printtup.c:360:16
frame #9: 0x0000000102b8fdac
postgres`ExecutePlan(queryDesc=0x0000000137010300, operation=CMD_SELECT,
sendTuples=true, numberTuples=0, direction=ForwardScanDirection,
dest=0x00000001170102d8) at execMain.c:1679:9
frame #10: 0x0000000102b8fb98
postgres`standard_ExecutorRun(queryDesc=0x0000000137010300,
direction=ForwardScanDirection, count=0, execute_once=false) at
execMain.c:360:3
frame #11: 0x0000000102b8f988
postgres`ExecutorRun(queryDesc=0x0000000137010300,
direction=ForwardScanDirection, count=0, execute_once=false) at
execMain.c:306:3
frame #12: 0x0000000102ee2bd4
postgres`PortalRunSelect(portal=0x000000012782c500, forward=true, count=0,
dest=0x00000001170102d8) at pquery.c:922:4
frame #13: 0x0000000102ee2568
postgres`PortalRun(portal=0x000000012782c500, count=9223372036854775807,
isTopLevel=true, run_once=true, dest=0x00000001170102d8,
altdest=0x00000001170102d8, qc=0x000000016d5f21b8) at pquery.c:766:18
frame #14: 0x0000000102edce9c
postgres`exec_simple_query(query_string="SELECT bar();") at
postgres.c:1278:10
frame #15: 0x0000000102edbf6c postgres`PostgresMain(dbname="postgres",
username="surya") at postgres.c:4767:7
frame #16: 0x0000000102ed3594 postgres`BackendMain(startup_data="",
startup_data_len=4) at backend_startup.c:106:2
frame #17: 0x0000000102daf8f8
postgres`postmaster_child_launch(child_type=B_BACKEND, startup_data="",
startup_data_len=4, client_sock=0x000000016d5f25b8) at
launch_backend.c:277:3
frame #18: 0x0000000102db7708
postgres`BackendStartup(client_sock=0x000000016d5f25b8) at
postmaster.c:3624:8
frame #19: 0x0000000102db4438 postgres`ServerLoop at postmaster.c:1678:6
frame #20: 0x0000000102db3324 postgres`PostmasterMain(argc=3,
argv=0x000060000321d420) at postmaster.c:1376:11
frame #21: 0x0000000102c369c0 postgres`main(argc=3,
argv=0x000060000321d420) at main.c:199:3
frame #22: 0x00000001838bab98 dyld`start + 6076
(lldb)
The crash happens because textout() is called on integer data, and it
interprets 1073741824 (2^30) as a memory pointer.
I set breakpoints at two critical points to trace the issue:
Breakpoint 1: ExpandedRecordGetDatum (when PL/pgSQL returns the record)
At this point, the record still has complete version information:
(lldb) p erh->er_tupdesc_id
(uint64) 2 // Record was created with version 2
(lldb) p assign_record_type_identifier(erh->er_typeid, erh->er_typmod)
(uint64) 4 // Current type is now version 4
(lldb) p erh->er_tupdesc->attrs[1].atttypid
(Oid) 23 // Field b was INT4 when record was created
(lldb) p ((TypeCacheEntry*)lookup_type_cache(erh->er_typeid,
0x00100))->tupDesc->attrs[1].atttypid
(Oid) 25 // Field b is now TEXT in current definition
Version mismatch detected (2 != 4). The record has integer data but the
type definition changed to TEXT.
Breakpoint 2: record_out (when converting record to text for output)
After ExpandedRecordGetDatum flattens the record to HeapTupleHeader, the
version information is lost:
(lldb) p tupType
(Oid) 32770 //Only type OID preserved
(lldb) p tupTypmod
(int32) -1 //Only typmod preserved
(lldb) p tupdesc->attrs[1].atttypid
(Oid) 25 // Uses current definition: TEXT
When ExpandedRecordHeader is flattened to HeapTupleHeader, HeapTupleHeader
only stores type OID and typmod but not the version identifier.
This returns the current type definition (version 4, field b = TEXT), but
the actual data is still from version 2 (field b = INT, value = 1073741824).
The crash happens at rowtypes.c, when record_out() calls textout() on field
b. Since textout() expects a text pointer but receives an integer, it tries
to dereference 0x40000000 (1073741824 (2^30)), causing a segfault
that leads to the crash.
I believe the fix should be in pl_exec.c before the record is returned. At
the point where we still have access to erh->er_tupdesc_id, and we can
compare erh->er_tupdesc_id with current tupDesc_identifier, if they differ,
the type was altered. For each field with changed type, apply conversion
using exec_cast_value().
If conversion fails or no cast exists, raise a proper error, if not return
the converted record with updated version
This prevents crashes by either converting the data (INT to TEXT which
should work) or raising a clean error message instead of a segfault.
I am working on a patch for this.
Kindly let me know your thoughts.
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-01-24 01:55 ` surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-01-24 01:55 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
Hi All,
I am working on a patch for this.
>
I built a patch on 17.6 postgres version.
The overall issue is:
When ALTER TYPE modifies column types, the type cache updates its
tupDesc_identifier.
However, existing ExpandedRecordHeader instances still reference the old
tupdesc.
When the record is returned and flattened, the output functions expect data
to match the new type definition but receive data in the old format,
causing type confusion (e.g., interpreting an integer as a text pointer).
This was causing a segfault and crashing the server.
In my solution (attached patch), I added convert_record_for_altered_type()
function which detects type changes by comparing er_tupdesc_id against the
current tupDesc_identifier.
When a mismatch is found, it tries to convert each field value to match the
new type definition, if this fails we error out.
convert_record_for_altered_type() function is called in exec_stmt_return()
and exec_stmt_return_next() before returning records.
I tested my patch and see the below output
postgres=# DROP FUNCTION IF EXISTS bar();
DROP FUNCTION
postgres=# DROP TYPE IF EXISTS foo CASCADE;
DROP TYPE
postgres=# DROP FUNCTION IF EXISTS bar1();
DROP FUNCTION
postgres=# DROP TYPE IF EXISTS foo1 CASCADE;
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=#
postgres=# SELECT bar();
bar
------------------
(123,1073741824)
(1 row)
postgres=# CREATE TYPE foo1 AS (a INT, b INT);
CREATE TYPE
postgres=#
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=#
postgres=# SELECT bar1();
bar1
-------
(1,2)
(1 row)
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; -- TEXT → INT
postgres$# RETURN r; -- Should get clean error (not crash)
postgres$# END;
postgres$# $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=#
postgres=# SELECT bar2();
ERROR: invalid input syntax for type integer: "hello"
CONTEXT: PL/pgSQL function bar2() line 6 at RETURN
postgres=#
Attachments:
[application/octet-stream] 0001-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (6.9K, ../../CAOVWO5rbwKgHWLYJMvKuvGxW9eFSk7LADk=ZxDEvwA1uTefvAg@mail.gmail.com/3-0001-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
download | inline diff:
From 2ed5b4181a026729169e92e8bcd175f62b1daab1 Mon Sep 17 00:00:00 2001
From: spoondla <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH] 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 | 161 +++++++++++++++++++++++++++++++++++
1 file changed, 161 insertions(+)
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index e7b0f2544b4..53add41882a 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -458,6 +458,9 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
PLpgSQL_expr *expr);
+static ExpandedRecordHeader *convert_record_for_altered_type(PLpgSQL_execstate *estate,
+ ExpandedRecordHeader *erh,
+ Oid rectypeid);
/* ----------
* plpgsql_exec_function Called by the call handler for
@@ -3251,6 +3254,15 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
/* If record is empty, we return NULL not a row of nulls */
if (rec->erh && !ExpandedRecordIsEmpty(rec->erh))
{
+ /*
+ * 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 = convert_record_for_altered_type(estate,
+ rec->erh,
+ rec->rectypeid);
estate->retval = ExpandedRecordGetDatum(rec->erh);
estate->retisnull = false;
estate->rettype = rec->rectypeid;
@@ -3404,6 +3416,16 @@ 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)
+ rec->erh = convert_record_for_altered_type(estate,
+ rec->erh,
+ rec->rectypeid);
+
/* If rec is null, try to convert it to a row of nulls */
if (rec->erh == NULL)
instantiate_empty_record_variable(estate, rec);
@@ -8897,3 +8919,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.
+ *
+ * Returns a (possibly new) ExpandedRecordHeader with data matching the
+ * current type definition.
+ */
+static ExpandedRecordHeader *
+convert_record_for_altered_type(PLpgSQL_execstate *estate,
+ ExpandedRecordHeader *erh,
+ Oid 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 erh;
+
+ /* 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 erh;
+
+ /*
+ * 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 original */
+ if (!need_conversion)
+ {
+ pfree(new_values);
+ pfree(new_nulls);
+ return erh;
+ }
+
+ /* Build new expanded record with converted values */
+ new_erh = make_expanded_record_from_typeid(rectypeid, -1,
+ estate->tuple_store_cxt ?
+ estate->tuple_store_cxt :
+ CurrentMemoryContext);
+ expanded_record_set_fields(new_erh, new_values, new_nulls, true);
+
+ return new_erh;
+}
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-01-26 22:04 ` surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-01-26 22:04 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
Hi All,
I also tested the patch on postgres 19, it shows the same working behavior
as postgres 17.
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-02-02 21:45 ` surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-02-02 21:45 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
Hi All,
I created a commitfest entry here at:
https://commitfest.postgresql.org/patch/6449/
>
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-02-26 23:55 ` surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-02-26 23:55 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
Hi All,
CFBot on the commitfest asked me to rebase my code, and during the rebase
(rebase to 17 Stable version) I realized my initial patch v1 might have
a memory management issue.
When conversion of type was needed, the old record object was never
properly freed, and the new record was being created in the wrong memory
context. v2 fixes this by passing the record variable directly to
convert_record_for_altered_type() and using assign_record_var() internally
to replace a record variable and this way correctly frees the old value,
transfers the new one into the right memory context.
Testing:
1) All 224 core regression tests pass
2) All 13 PL/pgSQL regression tests pass
3) All original test cases from the bug report produce correct results
(same results as my v1 patch) (adding them here again for quick reference)
and we no longer see the crash of the database.
psql (17.9)
Type "help" for help.
postgres=# DROP FUNCTION IF EXISTS bar();
DROP FUNCTION
postgres=# DROP TYPE IF EXISTS foo CASCADE;
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();
DROP FUNCTION
postgres=# DROP TYPE IF EXISTS foo1 CASCADE;
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 TYPE IF EXISTS foo2 CASCADE;
NOTICE: drop cascades to function bar2()
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=# quit
Regards,
Surya Poondla
>
Attachments:
[application/octet-stream] 0002-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (6.8K, ../../CAOVWO5pbgCVx0zgTr1mxZug2hoGwxZOk+-Owvwg0jaQv9JE3Fw@mail.gmail.com/3-0002-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
download | inline diff:
From 2cc161d3f8d1c081f0afd9a8438ab9358cbdbee3 Mon Sep 17 00:00:00 2001
From: spoondla <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v2] 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 | 165 ++++++++++++++++++++++++++++++++++-
1 file changed, 164 insertions(+), 1 deletion(-)
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 6b077febdc8..f2ff1aa25ec 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,22 @@ 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;
+
+ /*
+ * 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);
+ }
+ /* FALL THROUGH */
+
+ case PLPGSQL_DTYPE_ROW:
{
/* exec_eval_datum can handle these cases */
int32 rettypmod;
@@ -3390,6 +3406,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 +8907,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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-03-16 22:09 ` surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-03-16 22:09 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
Hi All,
Rebased on the latest 17 code and slightly refactored the code.
Regards,
Surya Poondla
Attachments:
[application/octet-stream] 0003-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch (7.0K, ../../CAOVWO5r19cctAFKbW24jfMKsD-pkyV21w+z7L3pCPxM1CArtjQ@mail.gmail.com/3-0003-Fix-bug-19382-server-crash-when-ALTER-TYPE-is-used-m.patch)
download | inline diff:
From 3289746d88fbca10712d43cc4f50fa2ce99e62c1 Mon Sep 17 00:00:00 2001
From: spoondla <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-03-19 05:00 ` surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-03-19 05:00 UTC (permalink / raw)
To: dllggyx@outlook.com; pgsql-bugs@lists.postgresql.org
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 <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-03-19 07:53 ` =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: songjinzhou @ 2026-03-19 07:53 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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
tsinghualucky912@foxmail.com
原始邮件
发件人:surya poondla <suryapoondla4@gmail.com>
发件时间:2026年3月19日 13:00
收件人:dllggyx <dllggyx@outlook.com>, pgsql-bugs <pgsql-bugs@lists.postgresql.org>
主题: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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
@ 2026-03-20 18:16 ` surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-03-20 18:16 UTC (permalink / raw)
To: songjinzhou <tsinghualucky912@foxmail.com>; +Cc: dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-04-02 11:18 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: Andrey Borodin @ 2026-04-02 11:18 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
Hi!
Thanks for working on this!
> On 20 Mar 2026, at 23:16, surya poondla <suryapoondla4@gmail.com> 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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
@ 2026-04-02 23:14 ` surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-04-02 23:14 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
Hi All,
Thanks for the review Andrey.
On Thu, Apr 2, 2026 at 4:18 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
> Hi!
>
> Thanks for working on this!
>
> > On 20 Mar 2026, at 23:16, surya poondla <suryapoondla4@gmail.com> 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 <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v4] Fix (bug #19382) server crash when ALTER TYPE is used
mid-transaction in PL/pgSQL
When ALTER TYPE changes a composite type's column types within a
transaction, PL/pgSQL record variables that were populated before
the ALTER still hold data in the old format. Returning such records
causes a crash because the output functions expect data matching the
new type definition, not the old one.
The crash manifested as a segmentation fault in record_out() when it
attempted to interpret integer data as a text pointer, due to the
mismatch between the stored data and the current type definition.
---
src/pl/plpgsql/src/pl_exec.c | 69 +++++++++++++++++++++++++++++++++++-
1 file changed, 68 insertions(+), 1 deletion(-)
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..6250c4a748b 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,7 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
PLpgSQL_expr *expr);
+static void check_record_type_not_altered(PLpgSQL_rec *rec);
/* ----------
* plpgsql_exec_function Called by the call handler for
@@ -3287,8 +3288,30 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
}
break;
- case PLPGSQL_DTYPE_ROW:
case PLPGSQL_DTYPE_REC:
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
+ int32 rettypmod;
+
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, raise an error to
+ * prevent crashes when outputting the record.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+ !ExpandedRecordIsEmpty(rec->erh))
+ check_record_type_not_altered(rec);
+
+ exec_eval_datum(estate,
+ retvar,
+ &estate->rettype,
+ &rettypmod,
+ &estate->retval,
+ &estate->retisnull);
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
{
/* exec_eval_datum can handle these cases */
int32 rettypmod;
@@ -3434,6 +3457,14 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
TupleDesc rec_tupdesc;
TupleConversionMap *tupmap;
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, raise an error to
+ * prevent crashes when storing to the tuplestore.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+ check_record_type_not_altered(rec);
+
/* If rec is null, try to convert it to a row of nulls */
if (rec->erh == NULL)
instantiate_empty_record_variable(estate, rec);
@@ -9216,3 +9247,39 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
return paramstr.data;
}
+
+/*
+ * check_record_type_not_altered
+ *
+ * Check if a record's composite type has been altered since the record
+ * was populated. If so, raise an error to prevent crashes that would
+ * occur when outputting data that no longer matches the current type
+ * definition.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+ ExpandedRecordHeader *erh = rec->erh;
+ TypeCacheEntry *typentry;
+
+ /* Nothing to do for anonymous RECORD type */
+ if (rec->rectypeid == RECORDOID)
+ return;
+
+ /* Get current type definition from typcache */
+ typentry = lookup_type_cache(rec->rectypeid,
+ TYPECACHE_TUPDESC |
+ TYPECACHE_DOMAIN_BASE_INFO);
+ if (typentry->typtype == TYPTYPE_DOMAIN)
+ typentry = lookup_type_cache(typentry->domainBaseType,
+ TYPECACHE_TUPDESC);
+
+ /* If type has changed since the record was populated, raise an error */
+ if (erh->er_tupdesc_id != typentry->tupDesc_identifier)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot return record variable \"%s\" after its composite type was altered",
+ rec->refname),
+ errdetail("ALTER TYPE changed the definition of type \"%s\" after the record was populated.",
+ format_type_be(rec->rectypeid))));
+}
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-04-04 12:42 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: Andrey Borodin @ 2026-04-04 12:42 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
> On 3 Apr 2026, at 04:14, surya poondla <suryapoondla4@gmail.com> 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.5K, ../../D9FAF41E-8082-47AF-B36E-4FB9EAF8378B@yandex-team.ru/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 <amborodin@acm.org>
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 <dllggyx@outlook.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
@ 2026-04-09 04:24 ` surya poondla <suryapoondla4@gmail.com>
2026-04-14 23:49 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
0 siblings, 2 replies; 30+ messages in thread
From: surya poondla @ 2026-04-09 04:24 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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 <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v5] Fix (bug #19382) server crash when ALTER TYPE is used
mid-transaction in PL/pgSQL
When ALTER TYPE changes a composite type's column types within a
transaction, PL/pgSQL record variables that were populated before
the ALTER still hold data in the old format. Returning such records
causes a crash because the output functions expect data matching the
new type definition, not the old one.
The crash manifested as a segmentation fault in record_out() when it
attempted to interpret integer data as a text pointer, due to the
mismatch between the stored data and the current type definition.
The fix snapshots tupDesc_identifier values for all composite types
reachable from a record variable's type (including nested composite
types) at assignment time. At RETURN/RETURN NEXT time, these
identifiers are compared against current values from the type cache.
If any have changed, an error is raised instead of risking a crash.
---
.../plpgsql/src/expected/plpgsql_record.out | 67 ++++++
src/pl/plpgsql/src/pl_exec.c | 209 +++++++++++++++++-
src/pl/plpgsql/src/plpgsql.h | 9 +
src/pl/plpgsql/src/sql/plpgsql_record.sql | 58 +++++
4 files changed, 342 insertions(+), 1 deletion(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 511f9e03c85..ad21f8bbf3f 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,70 @@ table two_int8s_tab;
(42,42)
(1 row)
+-- Tests for bug #19382: server crash when ALTER TYPE is used mid-transaction
+-- in PL/pgSQL. Record variables populated before ALTER TYPE must not be
+-- returned, as the stored data no longer matches the current type definition.
+-- Case 1: Direct composite type change (INT -> TEXT)
+create type bug19382_foo as (a int, b int);
+create function bug19382_test_direct() returns record as $$
+declare r bug19382_foo := row(123, power(2, 30));
+begin
+ alter type bug19382_foo alter attribute b type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_direct();
+ERROR: cannot return record variable "r" after composite type "bug19382_foo" was altered
+HINT: Reassign the record variable after ALTER TYPE.
+CONTEXT: PL/pgSQL function bug19382_test_direct() line 5 at RETURN
+drop function bug19382_test_direct();
+drop type bug19382_foo cascade;
+-- Case 2: Nested composite type change
+create type bug19382_inner as (x int, y int);
+create type bug19382_outer as (a int, b bug19382_inner);
+create function bug19382_test_nested() returns record as $$
+declare r bug19382_outer;
+begin
+ r := row(1, row(10, power(2, 30)::int4)::bug19382_inner)::bug19382_outer;
+ alter type bug19382_inner alter attribute y type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_nested();
+ERROR: cannot return record variable "r" after composite type "bug19382_inner" was altered
+HINT: Reassign the record variable after ALTER TYPE.
+CONTEXT: PL/pgSQL function bug19382_test_nested() line 6 at RETURN
+drop function bug19382_test_nested();
+drop type bug19382_outer cascade;
+drop type bug19382_inner cascade;
+-- Case 3: OUT parameter
+create type bug19382_foo1 as (a int, b int);
+create function bug19382_test_out(out r1 bug19382_foo1) as $$
+begin
+ r1 := row(1, 2);
+ alter type bug19382_foo1 alter attribute b type text;
+ return;
+end;
+$$ language plpgsql;
+select bug19382_test_out();
+ERROR: cannot return record variable "r1" after composite type "bug19382_foo1" was altered
+HINT: Reassign the record variable after ALTER TYPE.
+CONTEXT: PL/pgSQL function bug19382_test_out() line 5 at RETURN
+drop function bug19382_test_out();
+drop type bug19382_foo1 cascade;
+-- Case 4: No ALTER TYPE (baseline — must not error)
+create type bug19382_foo2 as (a int, b int);
+create function bug19382_test_baseline() returns bug19382_foo2 as $$
+declare r bug19382_foo2 := row(1, 2);
+begin
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_baseline();
+ bug19382_test_baseline
+------------------------
+ (1,2)
+(1 row)
+
+drop function bug19382_test_baseline();
+drop type bug19382_foo2;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..be3529445f9 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,12 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
PLpgSQL_expr *expr);
+static void check_record_type_not_altered(PLpgSQL_rec *rec);
+static void collect_composite_type_versions(Oid typid,
+ Oid **oids, uint64 **versions,
+ int *n, int *alloc);
+static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec);
/* ----------
* plpgsql_exec_function Called by the call handler for
@@ -3287,8 +3293,30 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
}
break;
- case PLPGSQL_DTYPE_ROW:
case PLPGSQL_DTYPE_REC:
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
+ int32 rettypmod;
+
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, raise an error to
+ * prevent crashes when outputting the record.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+ !ExpandedRecordIsEmpty(rec->erh))
+ check_record_type_not_altered(rec);
+
+ exec_eval_datum(estate,
+ retvar,
+ &estate->rettype,
+ &rettypmod,
+ &estate->retval,
+ &estate->retisnull);
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
{
/* exec_eval_datum can handle these cases */
int32 rettypmod;
@@ -3434,6 +3462,14 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
TupleDesc rec_tupdesc;
TupleConversionMap *tupmap;
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, raise an error to
+ * prevent crashes when storing to the tuplestore.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+ check_record_type_not_altered(rec);
+
/* If rec is null, try to convert it to a row of nulls */
if (rec->erh == NULL)
instantiate_empty_record_variable(estate, rec);
@@ -7042,6 +7078,10 @@ exec_move_row(PLpgSQL_execstate *estate,
if (rec->erh)
DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
rec->erh = NULL;
+ /* Clear composite type snapshot */
+ rec->nCompTypes = 0;
+ rec->compTypeOids = NULL;
+ rec->compTypeVersions = NULL;
}
return;
}
@@ -8967,6 +9007,9 @@ assign_record_var(PLpgSQL_execstate *estate, PLpgSQL_rec *rec,
/* ... and install the new */
rec->erh = erh;
+
+ /* Snapshot composite type versions for ALTER TYPE detection */
+ snapshot_record_composite_types(estate, rec);
}
/*
@@ -9216,3 +9259,167 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
return paramstr.data;
}
+
+/*
+ * check_record_type_not_altered
+ *
+ * Check if any composite type reachable from this record's type has been
+ * altered since the record was populated. If so, raise an error to prevent
+ * crashes that would occur when outputting data that no longer matches the
+ * current type definition.
+ *
+ * Uses the composite type version snapshot taken at record assignment time
+ * to detect changes in both the outermost type and any nested composite types.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+ int i;
+
+ /* Nothing to do for anonymous RECORD type or no snapshot */
+ if (rec->rectypeid == RECORDOID || rec->nCompTypes <= 0)
+ return;
+
+ for (i = 0; i < rec->nCompTypes; i++)
+ {
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(rec->compTypeOids[i], TYPECACHE_TUPDESC);
+
+ if (typentry->tupDesc_identifier != rec->compTypeVersions[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot return record variable \"%s\" after composite type \"%s\" was altered",
+ rec->refname,
+ format_type_be(rec->compTypeOids[i])),
+ errhint("Reassign the record variable after ALTER TYPE.")));
+ }
+}
+
+/*
+ * collect_composite_type_versions
+ *
+ * Recursively collect tupDesc_identifier values for a composite type and
+ * all composite types reachable from its attributes. Skips anonymous
+ * RECORD types and types already recorded (to prevent infinite recursion).
+ *
+ * oids/versions arrays are repalloc'd as needed; n/alloc updated in place.
+ */
+static void
+collect_composite_type_versions(Oid typid,
+ Oid **oids, uint64 **versions,
+ int *n, int *alloc)
+{
+ TypeCacheEntry *typentry;
+ TupleDesc tupdesc;
+ int i;
+
+ /* Skip if already recorded */
+ for (i = 0; i < *n; i++)
+ {
+ if ((*oids)[i] == typid)
+ return;
+ }
+
+ typentry = lookup_type_cache(typid, TYPECACHE_TUPDESC);
+
+ /* Grow arrays if needed */
+ if (*n >= *alloc)
+ {
+ *alloc *= 2;
+ *oids = repalloc(*oids, *alloc * sizeof(Oid));
+ *versions = repalloc(*versions, *alloc * sizeof(uint64));
+ }
+
+ (*oids)[*n] = typid;
+ (*versions)[*n] = typentry->tupDesc_identifier;
+ (*n)++;
+
+ tupdesc = typentry->tupDesc;
+ if (tupdesc == NULL)
+ return;
+
+ /* Recurse into composite-type attributes */
+ for (i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+ Oid attrtypid;
+ char typtype;
+
+ if (attr->attisdropped)
+ continue;
+
+ attrtypid = attr->atttypid;
+ if (attrtypid == RECORDOID)
+ continue;
+
+ typtype = get_typtype(attrtypid);
+
+ /* Resolve domain types to their base type */
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ attrtypid = getBaseType(attrtypid);
+ typtype = get_typtype(attrtypid);
+ }
+
+ if (typtype == TYPTYPE_COMPOSITE)
+ collect_composite_type_versions(attrtypid,
+ oids, versions, n, alloc);
+ }
+}
+
+/*
+ * snapshot_record_composite_types
+ *
+ * Take a snapshot of tupDesc_identifier values for all composite types
+ * reachable from the record's declared type. Called when a record variable
+ * is assigned a new value, so that check_record_type_not_altered() can
+ * detect mid-transaction ALTER TYPE at RETURN time.
+ */
+static void
+snapshot_record_composite_types(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec)
+{
+ MemoryContext oldcxt;
+ int alloc = 8;
+ int n = 0;
+ Oid *oids;
+ uint64 *versions;
+
+ /* Nothing to do for anonymous RECORD type */
+ if (rec->rectypeid == RECORDOID)
+ {
+ rec->nCompTypes = 0;
+ return;
+ }
+
+ oldcxt = MemoryContextSwitchTo(estate->datum_context);
+ oids = palloc(alloc * sizeof(Oid));
+ versions = palloc(alloc * sizeof(uint64));
+
+ collect_composite_type_versions(rec->rectypeid,
+ &oids, &versions, &n, &alloc);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ if (n > 0)
+ {
+ /* Free previous snapshot if any */
+ if (rec->compTypeOids)
+ pfree(rec->compTypeOids);
+ if (rec->compTypeVersions)
+ pfree(rec->compTypeVersions);
+
+ rec->nCompTypes = n;
+ rec->compTypeOids = oids;
+ rec->compTypeVersions = versions;
+ }
+ else
+ {
+ pfree(oids);
+ pfree(versions);
+ rec->nCompTypes = 0;
+ rec->compTypeOids = NULL;
+ rec->compTypeVersions = NULL;
+ }
+}
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..cf9a657613d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -435,6 +435,15 @@ typedef struct PLpgSQL_rec
/* We always store record variables as "expanded" records */
ExpandedRecordHeader *erh;
+
+ /*
+ * Composite type version snapshot for ALTER TYPE detection.
+ * Populated when the record is assigned; checked at RETURN time.
+ * Includes the outermost type and all nested composite types.
+ */
+ int nCompTypes;
+ Oid *compTypeOids;
+ uint64 *compTypeVersions;
} PLpgSQL_rec;
/*
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 4fbed38b8bb..95f40e15b2f 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,61 @@ insert into two_int8s_tab values (compresult(42));
-- reconnect so we lose any local knowledge of anonymous record types
\c -
table two_int8s_tab;
+
+-- Tests for bug #19382: server crash when ALTER TYPE is used mid-transaction
+-- in PL/pgSQL. Record variables populated before ALTER TYPE must not be
+-- returned, as the stored data no longer matches the current type definition.
+
+-- Case 1: Direct composite type change (INT -> TEXT)
+create type bug19382_foo as (a int, b int);
+create function bug19382_test_direct() returns record as $$
+declare r bug19382_foo := row(123, power(2, 30));
+begin
+ alter type bug19382_foo alter attribute b type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_direct();
+drop function bug19382_test_direct();
+drop type bug19382_foo cascade;
+
+-- Case 2: Nested composite type change
+create type bug19382_inner as (x int, y int);
+create type bug19382_outer as (a int, b bug19382_inner);
+create function bug19382_test_nested() returns record as $$
+declare r bug19382_outer;
+begin
+ r := row(1, row(10, power(2, 30)::int4)::bug19382_inner)::bug19382_outer;
+ alter type bug19382_inner alter attribute y type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_nested();
+drop function bug19382_test_nested();
+drop type bug19382_outer cascade;
+drop type bug19382_inner cascade;
+
+-- Case 3: OUT parameter
+create type bug19382_foo1 as (a int, b int);
+create function bug19382_test_out(out r1 bug19382_foo1) as $$
+begin
+ r1 := row(1, 2);
+ alter type bug19382_foo1 alter attribute b type text;
+ return;
+end;
+$$ language plpgsql;
+select bug19382_test_out();
+drop function bug19382_test_out();
+drop type bug19382_foo1 cascade;
+
+-- Case 4: No ALTER TYPE (baseline — must not error)
+create type bug19382_foo2 as (a int, b int);
+create function bug19382_test_baseline() returns bug19382_foo2 as $$
+declare r bug19382_foo2 := row(1, 2);
+begin
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_baseline();
+drop function bug19382_test_baseline();
+drop type bug19382_foo2;
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-04-14 23:49 ` surya poondla <suryapoondla4@gmail.com>
1 sibling, 0 replies; 30+ messages in thread
From: surya poondla @ 2026-04-14 23:49 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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 <s_poondla@apple.com>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v5] Fix (bug #19382) server crash when ALTER TYPE is used
mid-transaction in PL/pgSQL
When ALTER TYPE changes a composite type's column types within a
transaction, PL/pgSQL record variables that were populated before
the ALTER still hold data in the old format. Returning such records
causes a crash because the output functions expect data matching the
new type definition, not the old one.
The crash manifested as a segmentation fault in record_out() when it
attempted to interpret integer data as a text pointer, due to the
mismatch between the stored data and the current type definition.
The fix snapshots tupDesc_identifier values for all composite types
reachable from a record variable's type (including nested composite
types) at assignment time. At RETURN/RETURN NEXT time, these
identifiers are compared against current values from the type cache.
If any have changed, an error is raised instead of risking a crash.
---
.../plpgsql/src/expected/plpgsql_record.out | 67 ++++++
src/pl/plpgsql/src/pl_exec.c | 209 +++++++++++++++++-
src/pl/plpgsql/src/plpgsql.h | 9 +
src/pl/plpgsql/src/sql/plpgsql_record.sql | 58 +++++
4 files changed, 342 insertions(+), 1 deletion(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 511f9e03c85..ad21f8bbf3f 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,70 @@ table two_int8s_tab;
(42,42)
(1 row)
+-- Tests for bug #19382: server crash when ALTER TYPE is used mid-transaction
+-- in PL/pgSQL. Record variables populated before ALTER TYPE must not be
+-- returned, as the stored data no longer matches the current type definition.
+-- Case 1: Direct composite type change (INT -> TEXT)
+create type bug19382_foo as (a int, b int);
+create function bug19382_test_direct() returns record as $$
+declare r bug19382_foo := row(123, power(2, 30));
+begin
+ alter type bug19382_foo alter attribute b type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_direct();
+ERROR: cannot return record variable "r" after composite type "bug19382_foo" was altered
+HINT: Reassign the record variable after ALTER TYPE.
+CONTEXT: PL/pgSQL function bug19382_test_direct() line 5 at RETURN
+drop function bug19382_test_direct();
+drop type bug19382_foo cascade;
+-- Case 2: Nested composite type change
+create type bug19382_inner as (x int, y int);
+create type bug19382_outer as (a int, b bug19382_inner);
+create function bug19382_test_nested() returns record as $$
+declare r bug19382_outer;
+begin
+ r := row(1, row(10, power(2, 30)::int4)::bug19382_inner)::bug19382_outer;
+ alter type bug19382_inner alter attribute y type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_nested();
+ERROR: cannot return record variable "r" after composite type "bug19382_inner" was altered
+HINT: Reassign the record variable after ALTER TYPE.
+CONTEXT: PL/pgSQL function bug19382_test_nested() line 6 at RETURN
+drop function bug19382_test_nested();
+drop type bug19382_outer cascade;
+drop type bug19382_inner cascade;
+-- Case 3: OUT parameter
+create type bug19382_foo1 as (a int, b int);
+create function bug19382_test_out(out r1 bug19382_foo1) as $$
+begin
+ r1 := row(1, 2);
+ alter type bug19382_foo1 alter attribute b type text;
+ return;
+end;
+$$ language plpgsql;
+select bug19382_test_out();
+ERROR: cannot return record variable "r1" after composite type "bug19382_foo1" was altered
+HINT: Reassign the record variable after ALTER TYPE.
+CONTEXT: PL/pgSQL function bug19382_test_out() line 5 at RETURN
+drop function bug19382_test_out();
+drop type bug19382_foo1 cascade;
+-- Case 4: No ALTER TYPE (baseline — must not error)
+create type bug19382_foo2 as (a int, b int);
+create function bug19382_test_baseline() returns bug19382_foo2 as $$
+declare r bug19382_foo2 := row(1, 2);
+begin
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_baseline();
+ bug19382_test_baseline
+------------------------
+ (1,2)
+(1 row)
+
+drop function bug19382_test_baseline();
+drop type bug19382_foo2;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..be3529445f9 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,12 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
PLpgSQL_expr *expr);
+static void check_record_type_not_altered(PLpgSQL_rec *rec);
+static void collect_composite_type_versions(Oid typid,
+ Oid **oids, uint64 **versions,
+ int *n, int *alloc);
+static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec);
/* ----------
* plpgsql_exec_function Called by the call handler for
@@ -3287,8 +3293,30 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
}
break;
- case PLPGSQL_DTYPE_ROW:
case PLPGSQL_DTYPE_REC:
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
+ int32 rettypmod;
+
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, raise an error to
+ * prevent crashes when outputting the record.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+ !ExpandedRecordIsEmpty(rec->erh))
+ check_record_type_not_altered(rec);
+
+ exec_eval_datum(estate,
+ retvar,
+ &estate->rettype,
+ &rettypmod,
+ &estate->retval,
+ &estate->retisnull);
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
{
/* exec_eval_datum can handle these cases */
int32 rettypmod;
@@ -3434,6 +3462,14 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
TupleDesc rec_tupdesc;
TupleConversionMap *tupmap;
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, raise an error to
+ * prevent crashes when storing to the tuplestore.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+ check_record_type_not_altered(rec);
+
/* If rec is null, try to convert it to a row of nulls */
if (rec->erh == NULL)
instantiate_empty_record_variable(estate, rec);
@@ -7042,6 +7078,10 @@ exec_move_row(PLpgSQL_execstate *estate,
if (rec->erh)
DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
rec->erh = NULL;
+ /* Clear composite type snapshot */
+ rec->nCompTypes = 0;
+ rec->compTypeOids = NULL;
+ rec->compTypeVersions = NULL;
}
return;
}
@@ -8967,6 +9007,9 @@ assign_record_var(PLpgSQL_execstate *estate, PLpgSQL_rec *rec,
/* ... and install the new */
rec->erh = erh;
+
+ /* Snapshot composite type versions for ALTER TYPE detection */
+ snapshot_record_composite_types(estate, rec);
}
/*
@@ -9216,3 +9259,167 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
return paramstr.data;
}
+
+/*
+ * check_record_type_not_altered
+ *
+ * Check if any composite type reachable from this record's type has been
+ * altered since the record was populated. If so, raise an error to prevent
+ * crashes that would occur when outputting data that no longer matches the
+ * current type definition.
+ *
+ * Uses the composite type version snapshot taken at record assignment time
+ * to detect changes in both the outermost type and any nested composite types.
+ */
+static void
+check_record_type_not_altered(PLpgSQL_rec *rec)
+{
+ int i;
+
+ /* Nothing to do for anonymous RECORD type or no snapshot */
+ if (rec->rectypeid == RECORDOID || rec->nCompTypes <= 0)
+ return;
+
+ for (i = 0; i < rec->nCompTypes; i++)
+ {
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(rec->compTypeOids[i], TYPECACHE_TUPDESC);
+
+ if (typentry->tupDesc_identifier != rec->compTypeVersions[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot return record variable \"%s\" after composite type \"%s\" was altered",
+ rec->refname,
+ format_type_be(rec->compTypeOids[i])),
+ errhint("Reassign the record variable after ALTER TYPE.")));
+ }
+}
+
+/*
+ * collect_composite_type_versions
+ *
+ * Recursively collect tupDesc_identifier values for a composite type and
+ * all composite types reachable from its attributes. Skips anonymous
+ * RECORD types and types already recorded (to prevent infinite recursion).
+ *
+ * oids/versions arrays are repalloc'd as needed; n/alloc updated in place.
+ */
+static void
+collect_composite_type_versions(Oid typid,
+ Oid **oids, uint64 **versions,
+ int *n, int *alloc)
+{
+ TypeCacheEntry *typentry;
+ TupleDesc tupdesc;
+ int i;
+
+ /* Skip if already recorded */
+ for (i = 0; i < *n; i++)
+ {
+ if ((*oids)[i] == typid)
+ return;
+ }
+
+ typentry = lookup_type_cache(typid, TYPECACHE_TUPDESC);
+
+ /* Grow arrays if needed */
+ if (*n >= *alloc)
+ {
+ *alloc *= 2;
+ *oids = repalloc(*oids, *alloc * sizeof(Oid));
+ *versions = repalloc(*versions, *alloc * sizeof(uint64));
+ }
+
+ (*oids)[*n] = typid;
+ (*versions)[*n] = typentry->tupDesc_identifier;
+ (*n)++;
+
+ tupdesc = typentry->tupDesc;
+ if (tupdesc == NULL)
+ return;
+
+ /* Recurse into composite-type attributes */
+ for (i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+ Oid attrtypid;
+ char typtype;
+
+ if (attr->attisdropped)
+ continue;
+
+ attrtypid = attr->atttypid;
+ if (attrtypid == RECORDOID)
+ continue;
+
+ typtype = get_typtype(attrtypid);
+
+ /* Resolve domain types to their base type */
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ attrtypid = getBaseType(attrtypid);
+ typtype = get_typtype(attrtypid);
+ }
+
+ if (typtype == TYPTYPE_COMPOSITE)
+ collect_composite_type_versions(attrtypid,
+ oids, versions, n, alloc);
+ }
+}
+
+/*
+ * snapshot_record_composite_types
+ *
+ * Take a snapshot of tupDesc_identifier values for all composite types
+ * reachable from the record's declared type. Called when a record variable
+ * is assigned a new value, so that check_record_type_not_altered() can
+ * detect mid-transaction ALTER TYPE at RETURN time.
+ */
+static void
+snapshot_record_composite_types(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec)
+{
+ MemoryContext oldcxt;
+ int alloc = 8;
+ int n = 0;
+ Oid *oids;
+ uint64 *versions;
+
+ /* Nothing to do for anonymous RECORD type */
+ if (rec->rectypeid == RECORDOID)
+ {
+ rec->nCompTypes = 0;
+ return;
+ }
+
+ oldcxt = MemoryContextSwitchTo(estate->datum_context);
+ oids = palloc(alloc * sizeof(Oid));
+ versions = palloc(alloc * sizeof(uint64));
+
+ collect_composite_type_versions(rec->rectypeid,
+ &oids, &versions, &n, &alloc);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ if (n > 0)
+ {
+ /* Free previous snapshot if any */
+ if (rec->compTypeOids)
+ pfree(rec->compTypeOids);
+ if (rec->compTypeVersions)
+ pfree(rec->compTypeVersions);
+
+ rec->nCompTypes = n;
+ rec->compTypeOids = oids;
+ rec->compTypeVersions = versions;
+ }
+ else
+ {
+ pfree(oids);
+ pfree(versions);
+ rec->nCompTypes = 0;
+ rec->compTypeOids = NULL;
+ rec->compTypeVersions = NULL;
+ }
+}
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..cf9a657613d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -435,6 +435,15 @@ typedef struct PLpgSQL_rec
/* We always store record variables as "expanded" records */
ExpandedRecordHeader *erh;
+
+ /*
+ * Composite type version snapshot for ALTER TYPE detection.
+ * Populated when the record is assigned; checked at RETURN time.
+ * Includes the outermost type and all nested composite types.
+ */
+ int nCompTypes;
+ Oid *compTypeOids;
+ uint64 *compTypeVersions;
} PLpgSQL_rec;
/*
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 4fbed38b8bb..95f40e15b2f 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,61 @@ insert into two_int8s_tab values (compresult(42));
-- reconnect so we lose any local knowledge of anonymous record types
\c -
table two_int8s_tab;
+
+-- Tests for bug #19382: server crash when ALTER TYPE is used mid-transaction
+-- in PL/pgSQL. Record variables populated before ALTER TYPE must not be
+-- returned, as the stored data no longer matches the current type definition.
+
+-- Case 1: Direct composite type change (INT -> TEXT)
+create type bug19382_foo as (a int, b int);
+create function bug19382_test_direct() returns record as $$
+declare r bug19382_foo := row(123, power(2, 30));
+begin
+ alter type bug19382_foo alter attribute b type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_direct();
+drop function bug19382_test_direct();
+drop type bug19382_foo cascade;
+
+-- Case 2: Nested composite type change
+create type bug19382_inner as (x int, y int);
+create type bug19382_outer as (a int, b bug19382_inner);
+create function bug19382_test_nested() returns record as $$
+declare r bug19382_outer;
+begin
+ r := row(1, row(10, power(2, 30)::int4)::bug19382_inner)::bug19382_outer;
+ alter type bug19382_inner alter attribute y type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_nested();
+drop function bug19382_test_nested();
+drop type bug19382_outer cascade;
+drop type bug19382_inner cascade;
+
+-- Case 3: OUT parameter
+create type bug19382_foo1 as (a int, b int);
+create function bug19382_test_out(out r1 bug19382_foo1) as $$
+begin
+ r1 := row(1, 2);
+ alter type bug19382_foo1 alter attribute b type text;
+ return;
+end;
+$$ language plpgsql;
+select bug19382_test_out();
+drop function bug19382_test_out();
+drop type bug19382_foo1 cascade;
+
+-- Case 4: No ALTER TYPE (baseline — must not error)
+create type bug19382_foo2 as (a int, b int);
+create function bug19382_test_baseline() returns bug19382_foo2 as $$
+declare r bug19382_foo2 := row(1, 2);
+begin
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_baseline();
+drop function bug19382_test_baseline();
+drop type bug19382_foo2;
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-04-16 12:00 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
1 sibling, 1 reply; 30+ messages in thread
From: Andrey Borodin @ 2026-04-16 12:00 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
> On 9 Apr 2026, at 09:24, surya poondla <suryapoondla4@gmail.com> 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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
@ 2026-04-16 23:20 ` surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-04-16 23:20 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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 <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-04-17 06:57 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: Andrey Borodin @ 2026-04-17 06:57 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
> On 17 Apr 2026, at 04:20, surya poondla <suryapoondla4@gmail.com> 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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
@ 2026-04-23 04:17 ` surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-04-23 04:17 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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 <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-06-02 23:44 ` surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-06-02 23:44 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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 <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-06-30 20:50 ` surya poondla <suryapoondla4@gmail.com>
2026-07-06 22:16 ` Re: BUG #19382: Server crash at __nss_database_lookup Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-07 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 2 replies; 30+ messages in thread
From: surya poondla @ 2026-06-30 20:50 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
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 <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-07-06 22:16 ` Zsolt Parragi <zsolt.parragi@percona.com>
1 sibling, 0 replies; 30+ messages in thread
From: Zsolt Parragi @ 2026-07-06 22:16 UTC (permalink / raw)
To: pgsql-bugs@lists.postgresql.org
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-07-07 23:20 ` surya poondla <suryapoondla4@gmail.com>
2026-07-13 20:53 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
1 sibling, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-07-07 23:20 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>; Andrey Borodin <x4mmm@yandex-team.ru>
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 <s_poondla@apple.com>
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] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-07 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-07-13 20:53 ` surya poondla <suryapoondla4@gmail.com>
2026-07-13 22:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-07-13 20:53 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: songjinzhou <tsinghualucky912@foxmail.com>; dllggyx <dllggyx@outlook.com>; pgsql-bugs <pgsql-bugs@lists.postgresql.org>
Hi All,
I rebased the patch and split it into a series of smaller patch sets for
easier review.
Regards,
Surya Poondla
Attachments:
[application/octet-stream] v11_002-Refresh-composite-type-snapshot-on-in-place-record-r.patch (5.2K, ../../CAOVWO5pfjwcRKB8HNDXr-oc_yaWJmZUH2r0OAQdjxDGyYo8SQg@mail.gmail.com/3-v11_002-Refresh-composite-type-snapshot-on-in-place-record-r.patch)
download | inline diff:
From 3b4e48aa2068c00118b4b0a83f57109190ed7a58 Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:22:54 -0700
Subject: [PATCH v11 2/4] Refresh composite-type snapshot on in-place record
reassignment (bug #19382)
The previous commit added a snapshot of reachable composite types on
whole-record assignment (via assign_record_var). However, when the LHS
record variable already has an ExpandedRecord and the RHS has a
matching rowtype, exec_move_row_from_datum takes a fast path that
overwrites the tuple bytes in place via expanded_record_set_tuple and
returns without going through assign_record_var. On that path the
snapshot was not refreshed, leaving stale nested-type versions behind.
The visible symptom, reported by Zsolt Parragi:
r out2;
r := ROW(1, ROW(10, 20)::inn2)::out2;
ALTER TYPE inn2 ALTER ATTRIBUTE y TYPE text;
r := ROW(1, ROW(10, 'hello')::inn2)::out2; -- fresh valid data
RETURN r; -- master returns it;
-- previous patch errored
At RETURN the check compared the stale inn2 snapshot against the current
type-cache version, saw drift, and raised a false-positive error.
Add snapshot_record_composite_types() calls at the two in-place
set_tuple paths in exec_move_row_from_datum so any whole-record write
refreshes the snapshot. Reassignment with fresh data after an ALTER
now succeeds; reassignment without a follow-up write still errors.
---
.../plpgsql/src/expected/plpgsql_record.out | 23 +++++++++++++++++++
src/pl/plpgsql/src/pl_exec.c | 9 ++++++++
src/pl/plpgsql/src/sql/plpgsql_record.sql | 19 +++++++++++++++
3 files changed, 51 insertions(+)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 821aa4df90b..e6b5827ef91 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1017,3 +1017,26 @@ 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 9: 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;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 70578b87c7b..4fa32238330 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -7799,6 +7799,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;
}
@@ -7913,6 +7920,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;
}
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 0ee7a131eea..9f33e49118a 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -698,3 +698,22 @@ select bug19382_test_var_copy();
drop function bug19382_test_var_copy();
drop type bug19382_outer4 cascade;
drop type bug19382_inner4 cascade;
+
+-- Case 9: 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;
--
2.39.5 (Apple Git-154)
[application/octet-stream] v11_001-Detect-mid-transaction-ALTER-TYPE-on-PL-pgSQL-record.patch (22.0K, ../../CAOVWO5pfjwcRKB8HNDXr-oc_yaWJmZUH2r0OAQdjxDGyYo8SQg@mail.gmail.com/4-v11_001-Detect-mid-transaction-ALTER-TYPE-on-PL-pgSQL-record.patch)
download | inline diff:
From d5b36ec6ad2a9a7e5c6eeb7a987d06aea422bc14 Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:19:21 -0700
Subject: [PATCH v11 1/4] Detect mid-transaction ALTER TYPE on PL/pgSQL record
variables (bug #19382)
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.
This commit adds the detection mechanism: at assignment time we snapshot
tupDesc_identifier values for all composite types reachable from the
record variable's declared type; at read time we compare the snapshot
against current values from the type cache and raise an error if any
have changed.
The outermost type is checked via the ExpandedRecord's er_tupdesc_id,
which is set when the ER is created and covers field-by-field
assignment and SELECT INTO paths that do not rebuild the record
wholesale. Nested composite types are checked against the assignment-
time snapshot when available.
Handles named-composite record variables and named-composite fields of
records. Follow-up commits will extend coverage to generic RECORD
variables assigned via ::type casts, whole-record reassignment after
inner-type ALTER, and composite types reached through container types.
---
.../plpgsql/src/expected/plpgsql_record.out | 132 +++++++++
src/pl/plpgsql/src/pl_exec.c | 259 +++++++++++++++++-
src/pl/plpgsql/src/plpgsql.h | 10 +
src/pl/plpgsql/src/sql/plpgsql_record.sql | 121 ++++++++
4 files changed, 520 insertions(+), 2 deletions(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 511f9e03c85..821aa4df90b 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,135 @@ 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: 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;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..70578b87c7b 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,13 @@ 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 +3294,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 +3463,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 +5488,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 +7088,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 +7979,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 +9024,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);
}
/*
@@ -9209,10 +9269,205 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
convert_value_to_string(estate,
prm->value,
prm->ptype),
- -1);
+ -1);
}
MemoryContextSwitchTo(oldcontext);
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;
+ }
+
+ 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..faa7b46274b 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -435,6 +435,16 @@ typedef struct PLpgSQL_rec
/* We always store record variables as "expanded" records */
ExpandedRecordHeader *erh;
+
+ /*
+ * Snapshot of tupDesc_identifier values for all composite types reachable
+ * from the record's declared type (or, for RECORDOID variables, from the
+ * type adopted from the assigned value). Used to detect mid-transaction
+ * ALTER TYPE. Empty for anonymous rowtypes that cannot be versioned.
+ */
+ 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..0ee7a131eea 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,124 @@ 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: 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;
--
2.39.5 (Apple Git-154)
[application/octet-stream] v11_003-Cover-generic-RECORD-variables-in-ALTER-TYPE-detecti.patch (11.3K, ../../CAOVWO5pfjwcRKB8HNDXr-oc_yaWJmZUH2r0OAQdjxDGyYo8SQg@mail.gmail.com/5-v11_003-Cover-generic-RECORD-variables-in-ALTER-TYPE-detecti.patch)
download | inline diff:
From d4d6f39ee37e4e6b0378cdb17c11197230d97851 Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:24:32 -0700
Subject: [PATCH v11 3/4] Cover generic RECORD variables in ALTER TYPE
detection (bug #19382)
Previously the check keyed off the variable's declared type (rectypeid),
so a variable declared as generic RECORD -- whose rectypeid is RECORDOID
-- was silently skipped. Zsolt Parragi reported that the same crash
mechanism still fires in that case:
r record;
r := ROW(123, power(2,30)::int4)::foo;
ALTER TYPE foo ALTER ATTRIBUTE b TYPE text;
RETURN r; -- crashes on master and prior patches
Use an "effective root type" in both the snapshot and check functions.
For a named-composite variable the declared type is used, as before.
For a RECORD variable the type is taken from the ExpandedRecord's
er_typeid once the record has been assigned, so the actual composite
type adopted from the assigned value (via ::foo, INTO from a query
producing a named row, etc.) is tracked.
Truly anonymous rowtypes (er_typeid still RECORDOID) are still skipped,
since the type cache does not version them and there is nothing to
compare against.
The three call sites that guard on rec->rectypeid != RECORDOID (in
exec_stmt_return, exec_stmt_return_next, and exec_eval_datum) are
relaxed to drop that condition and let the check function decide
internally whether the outer-type comparison applies.
---
.../plpgsql/src/expected/plpgsql_record.out | 65 +++++++++++++++++++
src/pl/plpgsql/src/pl_exec.c | 42 +++++++++---
src/pl/plpgsql/src/sql/plpgsql_record.sql | 58 +++++++++++++++++
3 files changed, 155 insertions(+), 10 deletions(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index e6b5827ef91..d1b6ce06e36 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1040,3 +1040,68 @@ select bug19382_test_reassign();
drop function bug19382_test_reassign();
drop type bug19382_outer3 cascade;
drop type bug19382_inner3 cascade;
+-- Case 10: 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 11: 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 12: 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 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;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 4fa32238330..0cb1756760a 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -3304,7 +3304,7 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
* the record was populated. If so, raise an error to
* prevent crashes when outputting the record.
*/
- if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+ if (rec->erh != NULL &&
!ExpandedRecordIsEmpty(rec->erh))
check_record_type_not_altered(rec);
@@ -3468,7 +3468,7 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
* the record was populated. If so, raise an error to
* prevent crashes when storing to the tuplestore.
*/
- if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+ 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 */
@@ -5493,8 +5493,7 @@ exec_eval_datum(PLpgSQL_execstate *estate,
* the record was populated. This catches all output
* paths: RETURN, RAISE, EXECUTE USING, etc.
*/
- if (rec->rectypeid != RECORDOID &&
- !ExpandedRecordIsEmpty(rec->erh))
+ if (!ExpandedRecordIsEmpty(rec->erh))
check_record_type_not_altered(rec);
if (ExpandedRecordIsEmpty(rec->erh))
@@ -9308,8 +9307,17 @@ check_record_type_not_altered(PLpgSQL_rec *rec)
Oid check_typid;
int i;
- /* Nothing to do for anonymous RECORD type */
- if (rec->rectypeid == RECORDOID)
+ /*
+ * 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;
/*
@@ -9318,7 +9326,6 @@ check_record_type_not_altered(PLpgSQL_rec *rec)
* 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);
@@ -9432,19 +9439,34 @@ collect_composite_type_versions(Oid typid,
* 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;
- /* Nothing to do for anonymous RECORD type */
- if (rec->rectypeid == RECORDOID)
+ /*
+ * 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;
@@ -9454,7 +9476,7 @@ snapshot_record_composite_types(PLpgSQL_execstate *estate,
oids = palloc(alloc * sizeof(Oid));
versions = palloc(alloc * sizeof(uint64));
- collect_composite_type_versions(rec->rectypeid,
+ collect_composite_type_versions(root_typid,
&oids, &versions, &n, &alloc);
MemoryContextSwitchTo(oldcxt);
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 9f33e49118a..f2f9c85f226 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -717,3 +717,61 @@ select bug19382_test_reassign();
drop function bug19382_test_reassign();
drop type bug19382_outer3 cascade;
drop type bug19382_inner3 cascade;
+
+-- Case 10: 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 11: 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 12: 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 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;
--
2.39.5 (Apple Git-154)
[application/octet-stream] v11_004-Detect-composite-types-reached-through-container-typ.patch (10.6K, ../../CAOVWO5pfjwcRKB8HNDXr-oc_yaWJmZUH2r0OAQdjxDGyYo8SQg@mail.gmail.com/6-v11_004-Detect-composite-types-reached-through-container-typ.patch)
download | inline diff:
From 23b75747585819e3331045d702d65e5cc82e1daf Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:27:59 -0700
Subject: [PATCH v11 4/4] Detect composite types reached through container
types (bug #19382)
The type-tree walk in collect_composite_type_versions() only recursed
into an attribute when its typtype was TYPTYPE_COMPOSITE. A field
whose type is a container of composites -- for example t_comp[] -- has
typtype TYPTYPE_BASE (arrays are base types in PostgreSQL, not their
element type), so the recursion silently skipped it.
An ALTER TYPE on the element composite then went undetected, and
record_out() interpreted the stored bytes against the new tupdesc:
CREATE TYPE t_comp AS (f1 int);
CREATE TYPE t_parent AS (arr t_comp[]);
CREATE FUNCTION ar() RETURNS t_parent AS $$ DECLARE r t_parent;
BEGIN
r := ROW(ARRAY[ROW(1073741824)::t_comp]::t_comp[])::t_parent;
ALTER TYPE t_comp ALTER ATTRIBUTE f1 TYPE text;
RETURN r;
END; $$ LANGUAGE plpgsql;
SELECT ar(); -- master and prior patches: crash
The failure is exactly the same class as the original bug 19382, one
container layer deeper: record_out on t_parent recurses into array_out
on t_comp[], which calls record_out on t_comp with the current (text)
tupdesc, which calls textout on the stored int -> segfault in
text_to_cstring.
Replace the single "if TYPTYPE_COMPOSITE" check with a peel loop that
unwraps domain and container layers (array, range, multirange) until
it reaches a composite type (which is snapshotted) or something that
cannot contain one (which is skipped). The loop terminates because
each peel yields a strictly smaller type and PostgreSQL forbids
self-referential composites.
---
.../plpgsql/src/expected/plpgsql_record.out | 42 +++++++++++
src/pl/plpgsql/src/pl_exec.c | 72 ++++++++++++++-----
src/pl/plpgsql/src/plpgsql.h | 7 +-
src/pl/plpgsql/src/sql/plpgsql_record.sql | 37 ++++++++++
4 files changed, 136 insertions(+), 22 deletions(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index d1b6ce06e36..47484d9bbce 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1105,3 +1105,45 @@ ERROR: cannot return record variable "r" after composite type "bug19382_foo7" w
CONTEXT: PL/pgSQL function bug19382_test_generic_raise() line 6 at RAISE
drop function bug19382_test_generic_raise();
drop type bug19382_foo7 cascade;
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+ r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+ alter type bug19382_elem alter attribute f1 type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+ERROR: cannot return record variable "r" after composite type "bug19382_elem" was altered
+CONTEXT: PL/pgSQL function bug19382_test_array_elem() line 6 at RETURN
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+ r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ alter type bug19382_elem2 add attribute f2 text;
+ r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+ bug19382_test_array_reassign
+------------------------------
+ ("{""(1,hi)""}")
+(1 row)
+
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 0cb1756760a..9c0be5ac756 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -477,7 +477,6 @@ static void collect_composite_type_versions(Oid typid,
static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
PLpgSQL_rec *rec);
-
/* ----------
* plpgsql_exec_function Called by the call handler for
* function execution.
@@ -5489,9 +5488,9 @@ exec_eval_datum(PLpgSQL_execstate *estate,
else
{
/*
- * Check if the record's composite type was altered since
- * the record was populated. This catches all output
- * paths: RETURN, RAISE, EXECUTE USING, etc.
+ * Check if the record's composite type was altered
+ * since the record was populated. This catches all
+ * output paths: RETURN, RAISE, EXECUTE USING, etc.
*/
if (!ExpandedRecordIsEmpty(rec->erh))
check_record_type_not_altered(rec);
@@ -9277,7 +9276,7 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
convert_value_to_string(estate,
prm->value,
prm->ptype),
- -1);
+ -1);
}
MemoryContextSwitchTo(oldcontext);
@@ -9408,27 +9407,64 @@ collect_composite_type_versions(Oid typid,
{
Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
Oid attrtypid;
- char typtype;
if (attr->attisdropped)
continue;
attrtypid = attr->atttypid;
- if (attrtypid == RECORDOID)
- continue;
-
- typtype = get_typtype(attrtypid);
- /* Resolve domain types to their base type */
- if (typtype == TYPTYPE_DOMAIN)
+ /*
+ * A record field can reference a composite type directly, or wrap
+ * one inside a domain or a container type (array, range, or
+ * multirange). ALTER TYPE on such an indirectly-referenced
+ * composite must still be detected, so peel off domain and
+ * container layers until we reach a composite type (which we
+ * snapshot) or something that cannot contain one (which we skip).
+ * This terminates because each peel step yields a strictly
+ * "smaller" type. Container element types cannot be self-referential.
+ */
+ for (;;)
{
- attrtypid = getBaseType(attrtypid);
- typtype = get_typtype(attrtypid);
- }
+ char typtype = get_typtype(attrtypid);
+ Oid elemtypid;
+
+ if (attrtypid == RECORDOID)
+ break; /* anonymous rowtype: not versionable */
+
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ attrtypid = getBaseType(attrtypid);
+ continue;
+ }
- if (typtype == TYPTYPE_COMPOSITE)
- collect_composite_type_versions(attrtypid,
- oids, versions, n, alloc);
+ if (typtype == TYPTYPE_COMPOSITE)
+ {
+ collect_composite_type_versions(attrtypid,
+ oids, versions, n, alloc);
+ break;
+ }
+
+ /* Array element type? */
+ elemtypid = get_element_type(attrtypid);
+
+ /* Otherwise a range's subtype? */
+ if (!OidIsValid(elemtypid))
+ elemtypid = get_range_subtype(attrtypid);
+
+ /* Otherwise a multirange's range's subtype? */
+ if (!OidIsValid(elemtypid) && typtype == TYPTYPE_MULTIRANGE)
+ {
+ Oid rangetypid = get_multirange_range(attrtypid);
+
+ if (OidIsValid(rangetypid))
+ elemtypid = get_range_subtype(rangetypid);
+ }
+
+ if (!OidIsValid(elemtypid))
+ break; /* not a composite-bearing type */
+
+ attrtypid = elemtypid;
+ }
}
}
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index faa7b46274b..cf9a657613d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -437,10 +437,9 @@ typedef struct PLpgSQL_rec
ExpandedRecordHeader *erh;
/*
- * Snapshot of tupDesc_identifier values for all composite types reachable
- * from the record's declared type (or, for RECORDOID variables, from the
- * type adopted from the assigned value). Used to detect mid-transaction
- * ALTER TYPE. Empty for anonymous rowtypes that cannot be versioned.
+ * Composite type version snapshot for ALTER TYPE detection.
+ * Populated when the record is assigned; checked at RETURN time.
+ * Includes the outermost type and all nested composite types.
*/
int nCompTypes;
Oid *compTypeOids;
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index f2f9c85f226..ecb27ec6d60 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -775,3 +775,40 @@ $$ language plpgsql;
select bug19382_test_generic_raise();
drop function bug19382_test_generic_raise();
drop type bug19382_foo7 cascade;
+
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+ r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+ alter type bug19382_elem alter attribute f1 type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+ r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ alter type bug19382_elem2 add attribute f2 text;
+ r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-07 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-13 20:53 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-07-13 22:18 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-15 22:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
0 siblings, 1 reply; 30+ messages in thread
From: Zsolt Parragi @ 2026-07-13 22:18 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org, songjinzhou <tsinghualucky912@foxmail.com>
Hello!
The crash is still reproducible if an old type value is carried in an
array variable and then gets re-assigned:
create type foo as (a int, b int);
create function p1() returns foo as $$
declare arr foo[]; r foo;
begin
arr := array[row(123, power(2,30)::int4)::foo];
alter type foo alter attribute b type text;
r := arr[1];
return r;
end $$ language plpgsql;
select p1();
Or it crashes the same way if we return arr[1] directly, or if we
assign it to a field of another composite type.
And I also found another false positive:
create type foo3 as (a int, b int);
create function p3() returns foo3 as $$
declare r foo3;
begin
r := row(1, 2)::foo3;
alter type foo3 alter attribute b type text;
r := row(1, 'hello')::foo3;
return r;
end $$ language plpgsql;
select p3();
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-07 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-13 20:53 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-13 22:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Zsolt Parragi <zsolt.parragi@percona.com>
@ 2026-07-15 22:16 ` surya poondla <suryapoondla4@gmail.com>
2026-07-16 06:23 ` Re: BUG #19382: Server crash at __nss_database_lookup Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 1 reply; 30+ messages in thread
From: surya poondla @ 2026-07-15 22:16 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-bugs@lists.postgresql.org, songjinzhou <tsinghualucky912@foxmail.com>; Andrey Borodin <x4mmm@yandex-team.ru>
Hi Zsolt,
Thanks for the sharp testing. Attached is v12 with a fix for one of your
two cases; the other needs more design work than a small amendment.
Case 2 (outer-type ALTER + reassign), fixed in v12. The false positive came
from er_tupdesc_id (set once at ExpandedRecord creation)
being left stale when the record was reassigned in place via
expanded_record_set_tuple.
v12 refreshes it at the same two in-place set_tuple sites where I already
added the nested-snapshot refresh.
Reassignment with fresh data now returns the row (matching master);
reassignment without a follow-up write still errors.
Case 1 (array-carried composite in a scalar PL/pgSQL variable) still open.
Confirmed still crashes on v12. The v11/v12 snapshot lives on PLpgSQL_rec;
here the stale bytes flow through arr foo[], which is a
PLpgSQL_var that is outside the current mechanism's scope.
A small point-fix doesn't cover this properly, because the same shape
extends to any
composite Datum that predates an ALTER (cursors, function args, temp table
rows, etc.).
I'd rather propose a design for this separately, likely a session-scoped
set of "types altered in this transaction" plus
a check at composite-read time. Happy to open a fresh thread on it.
Regards,
Surya Poondla
Attachments:
[application/octet-stream] v12_003-Cover-generic-RECORD-variables-in-ALTER-TYPE-detecti.patch (11.3K, ../../CAOVWO5rJ8sAtrBytkTmawjpBpOR453zYh7n3vaj5FfWx=bMM5w@mail.gmail.com/3-v12_003-Cover-generic-RECORD-variables-in-ALTER-TYPE-detecti.patch)
download | inline diff:
From eb00d945289e8a7aec2dc5513dff2e32e8c52f25 Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:24:32 -0700
Subject: [PATCH v12 3/4] Cover generic RECORD variables in ALTER TYPE
detection (bug #19382)
Previously the check keyed off the variable's declared type (rectypeid),
so a variable declared as generic RECORD -- whose rectypeid is RECORDOID
-- was silently skipped. Zsolt Parragi reported that the same crash
mechanism still fires in that case:
r record;
r := ROW(123, power(2,30)::int4)::foo;
ALTER TYPE foo ALTER ATTRIBUTE b TYPE text;
RETURN r; -- crashes on master and prior patches
Use an "effective root type" in both the snapshot and check functions.
For a named-composite variable the declared type is used, as before.
For a RECORD variable the type is taken from the ExpandedRecord's
er_typeid once the record has been assigned, so the actual composite
type adopted from the assigned value (via ::foo, INTO from a query
producing a named row, etc.) is tracked.
Truly anonymous rowtypes (er_typeid still RECORDOID) are still skipped,
since the type cache does not version them and there is nothing to
compare against.
The three call sites that guard on rec->rectypeid != RECORDOID (in
exec_stmt_return, exec_stmt_return_next, and exec_eval_datum) are
relaxed to drop that condition and let the check function decide
internally whether the outer-type comparison applies.
---
.../plpgsql/src/expected/plpgsql_record.out | 65 +++++++++++++++++++
src/pl/plpgsql/src/pl_exec.c | 42 +++++++++---
src/pl/plpgsql/src/sql/plpgsql_record.sql | 58 +++++++++++++++++
3 files changed, 155 insertions(+), 10 deletions(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 11bb2ae50f6..1ac7cfd147c 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1062,3 +1062,68 @@ select bug19382_test_outer_reassign();
drop function bug19382_test_outer_reassign();
drop type bug19382_foo_outer_reassign cascade;
+-- Case 10: 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 11: 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 12: 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 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;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 46f03d08ef2..073ace2d932 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -3305,7 +3305,7 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
* the record was populated. If so, raise an error to
* prevent crashes when outputting the record.
*/
- if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+ if (rec->erh != NULL &&
!ExpandedRecordIsEmpty(rec->erh))
check_record_type_not_altered(rec);
@@ -3469,7 +3469,7 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
* the record was populated. If so, raise an error to
* prevent crashes when storing to the tuplestore.
*/
- if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+ 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 */
@@ -5494,8 +5494,7 @@ exec_eval_datum(PLpgSQL_execstate *estate,
* the record was populated. This catches all output
* paths: RETURN, RAISE, EXECUTE USING, etc.
*/
- if (rec->rectypeid != RECORDOID &&
- !ExpandedRecordIsEmpty(rec->erh))
+ if (!ExpandedRecordIsEmpty(rec->erh))
check_record_type_not_altered(rec);
if (ExpandedRecordIsEmpty(rec->erh))
@@ -9313,8 +9312,17 @@ check_record_type_not_altered(PLpgSQL_rec *rec)
Oid check_typid;
int i;
- /* Nothing to do for anonymous RECORD type */
- if (rec->rectypeid == RECORDOID)
+ /*
+ * 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;
/*
@@ -9323,7 +9331,6 @@ check_record_type_not_altered(PLpgSQL_rec *rec)
* 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);
@@ -9437,19 +9444,34 @@ collect_composite_type_versions(Oid typid,
* 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;
- /* Nothing to do for anonymous RECORD type */
- if (rec->rectypeid == RECORDOID)
+ /*
+ * 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;
@@ -9459,7 +9481,7 @@ snapshot_record_composite_types(PLpgSQL_execstate *estate,
oids = palloc(alloc * sizeof(Oid));
versions = palloc(alloc * sizeof(uint64));
- collect_composite_type_versions(rec->rectypeid,
+ collect_composite_type_versions(root_typid,
&oids, &versions, &n, &alloc);
MemoryContextSwitchTo(oldcxt);
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 4e92af5c845..dacf17678f6 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -735,3 +735,61 @@ $$ language plpgsql;
select bug19382_test_outer_reassign();
drop function bug19382_test_outer_reassign();
drop type bug19382_foo_outer_reassign cascade;
+
+-- Case 10: 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 11: 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 12: 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 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;
--
2.39.5 (Apple Git-154)
[application/octet-stream] v12_004-Detect-composite-types-reached-through-container-typ.patch (10.6K, ../../CAOVWO5rJ8sAtrBytkTmawjpBpOR453zYh7n3vaj5FfWx=bMM5w@mail.gmail.com/4-v12_004-Detect-composite-types-reached-through-container-typ.patch)
download | inline diff:
From 89fb897ffff093871069fafeceb16ce1d6486ffb Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:27:59 -0700
Subject: [PATCH v12 4/4] Detect composite types reached through container
types (bug #19382)
The type-tree walk in collect_composite_type_versions() only recursed
into an attribute when its typtype was TYPTYPE_COMPOSITE. A field
whose type is a container of composites -- for example t_comp[] -- has
typtype TYPTYPE_BASE (arrays are base types in PostgreSQL, not their
element type), so the recursion silently skipped it.
An ALTER TYPE on the element composite then went undetected, and
record_out() interpreted the stored bytes against the new tupdesc:
CREATE TYPE t_comp AS (f1 int);
CREATE TYPE t_parent AS (arr t_comp[]);
CREATE FUNCTION ar() RETURNS t_parent AS $$ DECLARE r t_parent;
BEGIN
r := ROW(ARRAY[ROW(1073741824)::t_comp]::t_comp[])::t_parent;
ALTER TYPE t_comp ALTER ATTRIBUTE f1 TYPE text;
RETURN r;
END; $$ LANGUAGE plpgsql;
SELECT ar(); -- master and prior patches: crash
The failure is exactly the same class as the original bug 19382, one
container layer deeper: record_out on t_parent recurses into array_out
on t_comp[], which calls record_out on t_comp with the current (text)
tupdesc, which calls textout on the stored int -> segfault in
text_to_cstring.
Replace the single "if TYPTYPE_COMPOSITE" check with a peel loop that
unwraps domain and container layers (array, range, multirange) until
it reaches a composite type (which is snapshotted) or something that
cannot contain one (which is skipped). The loop terminates because
each peel yields a strictly smaller type and PostgreSQL forbids
self-referential composites.
---
.../plpgsql/src/expected/plpgsql_record.out | 42 +++++++++++
src/pl/plpgsql/src/pl_exec.c | 72 ++++++++++++++-----
src/pl/plpgsql/src/plpgsql.h | 7 +-
src/pl/plpgsql/src/sql/plpgsql_record.sql | 37 ++++++++++
4 files changed, 136 insertions(+), 22 deletions(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 1ac7cfd147c..8b77c99cfd5 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1127,3 +1127,45 @@ ERROR: cannot return record variable "r" after composite type "bug19382_foo7" w
CONTEXT: PL/pgSQL function bug19382_test_generic_raise() line 6 at RAISE
drop function bug19382_test_generic_raise();
drop type bug19382_foo7 cascade;
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+ r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+ alter type bug19382_elem alter attribute f1 type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+ERROR: cannot return record variable "r" after composite type "bug19382_elem" was altered
+CONTEXT: PL/pgSQL function bug19382_test_array_elem() line 6 at RETURN
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+ r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ alter type bug19382_elem2 add attribute f2 text;
+ r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+ bug19382_test_array_reassign
+------------------------------
+ ("{""(1,hi)""}")
+(1 row)
+
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 073ace2d932..01645db8ca3 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -478,7 +478,6 @@ static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
PLpgSQL_rec *rec);
static void refresh_erh_tupdesc_id(ExpandedRecordHeader *erh);
-
/* ----------
* plpgsql_exec_function Called by the call handler for
* function execution.
@@ -5490,9 +5489,9 @@ exec_eval_datum(PLpgSQL_execstate *estate,
else
{
/*
- * Check if the record's composite type was altered since
- * the record was populated. This catches all output
- * paths: RETURN, RAISE, EXECUTE USING, etc.
+ * Check if the record's composite type was altered
+ * since the record was populated. This catches all
+ * output paths: RETURN, RAISE, EXECUTE USING, etc.
*/
if (!ExpandedRecordIsEmpty(rec->erh))
check_record_type_not_altered(rec);
@@ -9282,7 +9281,7 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
convert_value_to_string(estate,
prm->value,
prm->ptype),
- -1);
+ -1);
}
MemoryContextSwitchTo(oldcontext);
@@ -9413,27 +9412,64 @@ collect_composite_type_versions(Oid typid,
{
Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
Oid attrtypid;
- char typtype;
if (attr->attisdropped)
continue;
attrtypid = attr->atttypid;
- if (attrtypid == RECORDOID)
- continue;
-
- typtype = get_typtype(attrtypid);
- /* Resolve domain types to their base type */
- if (typtype == TYPTYPE_DOMAIN)
+ /*
+ * A record field can reference a composite type directly, or wrap
+ * one inside a domain or a container type (array, range, or
+ * multirange). ALTER TYPE on such an indirectly-referenced
+ * composite must still be detected, so peel off domain and
+ * container layers until we reach a composite type (which we
+ * snapshot) or something that cannot contain one (which we skip).
+ * This terminates because each peel step yields a strictly
+ * "smaller" type. Container element types cannot be self-referential.
+ */
+ for (;;)
{
- attrtypid = getBaseType(attrtypid);
- typtype = get_typtype(attrtypid);
- }
+ char typtype = get_typtype(attrtypid);
+ Oid elemtypid;
+
+ if (attrtypid == RECORDOID)
+ break; /* anonymous rowtype: not versionable */
+
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ attrtypid = getBaseType(attrtypid);
+ continue;
+ }
- if (typtype == TYPTYPE_COMPOSITE)
- collect_composite_type_versions(attrtypid,
- oids, versions, n, alloc);
+ if (typtype == TYPTYPE_COMPOSITE)
+ {
+ collect_composite_type_versions(attrtypid,
+ oids, versions, n, alloc);
+ break;
+ }
+
+ /* Array element type? */
+ elemtypid = get_element_type(attrtypid);
+
+ /* Otherwise a range's subtype? */
+ if (!OidIsValid(elemtypid))
+ elemtypid = get_range_subtype(attrtypid);
+
+ /* Otherwise a multirange's range's subtype? */
+ if (!OidIsValid(elemtypid) && typtype == TYPTYPE_MULTIRANGE)
+ {
+ Oid rangetypid = get_multirange_range(attrtypid);
+
+ if (OidIsValid(rangetypid))
+ elemtypid = get_range_subtype(rangetypid);
+ }
+
+ if (!OidIsValid(elemtypid))
+ break; /* not a composite-bearing type */
+
+ attrtypid = elemtypid;
+ }
}
}
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index faa7b46274b..cf9a657613d 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -437,10 +437,9 @@ typedef struct PLpgSQL_rec
ExpandedRecordHeader *erh;
/*
- * Snapshot of tupDesc_identifier values for all composite types reachable
- * from the record's declared type (or, for RECORDOID variables, from the
- * type adopted from the assigned value). Used to detect mid-transaction
- * ALTER TYPE. Empty for anonymous rowtypes that cannot be versioned.
+ * Composite type version snapshot for ALTER TYPE detection.
+ * Populated when the record is assigned; checked at RETURN time.
+ * Includes the outermost type and all nested composite types.
*/
int nCompTypes;
Oid *compTypeOids;
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index dacf17678f6..5b97bedca39 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -793,3 +793,40 @@ $$ language plpgsql;
select bug19382_test_generic_raise();
drop function bug19382_test_generic_raise();
drop type bug19382_foo7 cascade;
+
+-- Case 14: composite type reachable through an array attribute.
+-- The element composite type must be tracked even though the attribute's
+-- own type is an array; otherwise ALTER TYPE on the element goes undetected
+-- and record_out() crashes reinterpreting the stored bytes.
+create type bug19382_elem as (f1 int);
+create type bug19382_arrparent as (arr bug19382_elem[]);
+create function bug19382_test_array_elem() returns bug19382_arrparent as $$
+declare r bug19382_arrparent;
+begin
+ r := row(array[row(power(2, 30)::int4)::bug19382_elem]::bug19382_elem[])::bug19382_arrparent;
+ alter type bug19382_elem alter attribute f1 type text;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_elem();
+drop function bug19382_test_array_elem();
+drop type bug19382_arrparent cascade;
+drop type bug19382_elem cascade;
+
+-- Case 15: array element composite, whole-record reassignment after ALTER
+-- with fresh matching data must succeed (no false positive on arrays).
+create type bug19382_elem2 as (f1 int);
+create type bug19382_arrparent2 as (arr bug19382_elem2[]);
+create function bug19382_test_array_reassign() returns bug19382_arrparent2 as $$
+declare r bug19382_arrparent2;
+begin
+ r := row(array[row(1)::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ alter type bug19382_elem2 add attribute f2 text;
+ r := row(array[row(1, 'hi')::bug19382_elem2]::bug19382_elem2[])::bug19382_arrparent2;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_array_reassign();
+drop function bug19382_test_array_reassign();
+drop type bug19382_arrparent2 cascade;
+drop type bug19382_elem2 cascade;
--
2.39.5 (Apple Git-154)
[application/octet-stream] v12_002-Refresh-composite-type-state-on-in-place-record-reas.patch (9.5K, ../../CAOVWO5rJ8sAtrBytkTmawjpBpOR453zYh7n3vaj5FfWx=bMM5w@mail.gmail.com/5-v12_002-Refresh-composite-type-state-on-in-place-record-reas.patch)
download | inline diff:
From e0dce474bcdeb0155abe322b7f7d2e9b4ba65635 Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:22:54 -0700
Subject: [PATCH v12 2/4] Refresh composite-type state on in-place record
reassignment (bug #19382)
The previous commit added a snapshot of reachable composite types on
whole-record assignment (via assign_record_var). However, when the LHS
record variable already has an ExpandedRecord and the RHS has a
matching rowtype, exec_move_row_from_datum takes a fast path that
overwrites the tuple bytes in place via expanded_record_set_tuple and
returns without going through assign_record_var. On that path both the
composite-type snapshot and the ExpandedRecord's outer-type identifier
(er_tupdesc_id) were left stale.
Two symptoms result, both reported by Zsolt Parragi.
1. False positive on inner-type ALTER + reassign
r out2;
r := ROW(1, ROW(10, 20)::inn2)::out2;
ALTER TYPE inn2 ALTER ATTRIBUTE y TYPE text;
r := ROW(1, ROW(10, 'hello')::inn2)::out2; -- fresh valid data
RETURN r; -- errored under prior patch
At RETURN the check compared the stale inn2 snapshot against the
current type-cache version, saw drift, and raised.
2. False positive on outer-type ALTER + reassign
r foo3;
r := ROW(1, 2)::foo3;
ALTER TYPE foo3 ALTER ATTRIBUTE b TYPE text;
r := ROW(1, 'hello')::foo3; -- fresh valid data
RETURN r; -- master returns it;
-- prior patch errored
At RETURN the outer-type check compared rec->erh->er_tupdesc_id (set
when the ER was created, hence stale) against the current type-cache
version and raised, even though the freshly-installed bytes match the
current type.
Add snapshot_record_composite_types() calls at the two in-place set_tuple
paths in exec_move_row_from_datum so any whole-record write refreshes
the nested snapshot. In addition, refresh_erh_tupdesc_id() re-fetches
the outer-type identifier from the type cache at the same points so the
outer-type check compares current-vs-current. Field-level writes do not
call this refresh, so partially-stale records are still caught by the
outer check.
---
.../plpgsql/src/expected/plpgsql_record.out | 45 ++++++++++++++++++
src/pl/plpgsql/src/pl_exec.c | 46 +++++++++++++++++++
src/pl/plpgsql/src/sql/plpgsql_record.sql | 37 +++++++++++++++
3 files changed, 128 insertions(+)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 821aa4df90b..11bb2ae50f6 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -1017,3 +1017,48 @@ 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 9: 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 9 (outer-type variant): Whole-record reassignment after ALTER TYPE on
+-- the record's own type. The reassigned bytes match the post-ALTER definition;
+-- the outer-type identifier (er_tupdesc_id) on the ExpandedRecord must be
+-- refreshed so check_record_type_not_altered() does not falsely reject.
+create type bug19382_foo_outer_reassign as (a int, b int);
+create function bug19382_test_outer_reassign() returns bug19382_foo_outer_reassign as $$
+declare r bug19382_foo_outer_reassign;
+begin
+ r := row(1, 2)::bug19382_foo_outer_reassign;
+ alter type bug19382_foo_outer_reassign alter attribute b type text;
+ r := row(1, 'hello')::bug19382_foo_outer_reassign;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_outer_reassign();
+ bug19382_test_outer_reassign
+------------------------------
+ (1,hello)
+(1 row)
+
+drop function bug19382_test_outer_reassign();
+drop type bug19382_foo_outer_reassign cascade;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 70578b87c7b..46f03d08ef2 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -476,6 +476,7 @@ static void collect_composite_type_versions(Oid typid,
int *n, int *alloc);
static void snapshot_record_composite_types(PLpgSQL_execstate *estate,
PLpgSQL_rec *rec);
+static void refresh_erh_tupdesc_id(ExpandedRecordHeader *erh);
/* ----------
@@ -7799,6 +7800,16 @@ 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
+ * outer-type identifier and the composite-type snapshot so
+ * that check_record_type_not_altered() compares against the
+ * current type cache: a whole-record reassignment installs
+ * bytes built under the current type, so any residual stale
+ * versioning from ER creation would produce false positives.
+ */
+ refresh_erh_tupdesc_id(rec->erh);
+ snapshot_record_composite_types(estate, rec);
return;
}
@@ -7913,6 +7924,9 @@ 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. */
+ refresh_erh_tupdesc_id(rec->erh);
+ snapshot_record_composite_types(estate, rec);
return;
}
@@ -9471,3 +9485,35 @@ snapshot_record_composite_types(PLpgSQL_execstate *estate,
rec->compTypeVersions = NULL;
}
}
+
+/*
+ * refresh_erh_tupdesc_id
+ *
+ * When an ExpandedRecord's tuple bytes are replaced in place (via
+ * expanded_record_set_tuple) with fresh data built under the current type
+ * definition, its er_tupdesc_id is left at the value assigned when the ER was
+ * created. That value becomes stale after mid-transaction ALTER TYPE, causing
+ * check_record_type_not_altered() to falsely reject the freshly-installed
+ * bytes as if they still reflected the old definition.
+ *
+ * Refresh er_tupdesc_id from the current type cache so the subsequent outer-
+ * type check compares current-vs-current. Only whole-record replacement paths
+ * should call this; field-level writes must leave er_tupdesc_id stale so the
+ * check still fires on the unwritten fields that hold pre-ALTER bytes.
+ */
+static void
+refresh_erh_tupdesc_id(ExpandedRecordHeader *erh)
+{
+ Oid resolved;
+ TypeCacheEntry *typentry;
+
+ if (erh->er_typeid == RECORDOID)
+ return; /* anonymous rowtype: not versionable */
+
+ resolved = erh->er_typeid;
+ if (get_typtype(resolved) == TYPTYPE_DOMAIN)
+ resolved = getBaseType(resolved);
+
+ typentry = lookup_type_cache(resolved, TYPECACHE_TUPDESC);
+ erh->er_tupdesc_id = typentry->tupDesc_identifier;
+}
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 0ee7a131eea..4e92af5c845 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -698,3 +698,40 @@ select bug19382_test_var_copy();
drop function bug19382_test_var_copy();
drop type bug19382_outer4 cascade;
drop type bug19382_inner4 cascade;
+
+-- Case 9: 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 9 (outer-type variant): Whole-record reassignment after ALTER TYPE on
+-- the record's own type. The reassigned bytes match the post-ALTER definition;
+-- the outer-type identifier (er_tupdesc_id) on the ExpandedRecord must be
+-- refreshed so check_record_type_not_altered() does not falsely reject.
+create type bug19382_foo_outer_reassign as (a int, b int);
+create function bug19382_test_outer_reassign() returns bug19382_foo_outer_reassign as $$
+declare r bug19382_foo_outer_reassign;
+begin
+ r := row(1, 2)::bug19382_foo_outer_reassign;
+ alter type bug19382_foo_outer_reassign alter attribute b type text;
+ r := row(1, 'hello')::bug19382_foo_outer_reassign;
+ return r;
+end;
+$$ language plpgsql;
+select bug19382_test_outer_reassign();
+drop function bug19382_test_outer_reassign();
+drop type bug19382_foo_outer_reassign cascade;
--
2.39.5 (Apple Git-154)
[application/octet-stream] v12_001-Detect-mid-transaction-ALTER-TYPE-on-PL-pgSQL-record.patch (22.0K, ../../CAOVWO5rJ8sAtrBytkTmawjpBpOR453zYh7n3vaj5FfWx=bMM5w@mail.gmail.com/6-v12_001-Detect-mid-transaction-ALTER-TYPE-on-PL-pgSQL-record.patch)
download | inline diff:
From d141f5d5d17fa6f70eb6e1f63bbcba3d35f2605d Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
Date: Mon, 13 Jul 2026 13:19:21 -0700
Subject: [PATCH v12 1/4] Detect mid-transaction ALTER TYPE on PL/pgSQL record
variables (bug #19382)
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.
This commit adds the detection mechanism: at assignment time we snapshot
tupDesc_identifier values for all composite types reachable from the
record variable's declared type; at read time we compare the snapshot
against current values from the type cache and raise an error if any
have changed.
The outermost type is checked via the ExpandedRecord's er_tupdesc_id,
which is set when the ER is created and covers field-by-field
assignment and SELECT INTO paths that do not rebuild the record
wholesale. Nested composite types are checked against the assignment-
time snapshot when available.
Handles named-composite record variables and named-composite fields of
records. Follow-up commits will extend coverage to generic RECORD
variables assigned via ::type casts, whole-record reassignment after
inner-type ALTER, and composite types reached through container types.
---
.../plpgsql/src/expected/plpgsql_record.out | 132 +++++++++
src/pl/plpgsql/src/pl_exec.c | 259 +++++++++++++++++-
src/pl/plpgsql/src/plpgsql.h | 10 +
src/pl/plpgsql/src/sql/plpgsql_record.sql | 121 ++++++++
4 files changed, 520 insertions(+), 2 deletions(-)
diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index 511f9e03c85..821aa4df90b 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -885,3 +885,135 @@ 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: 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;
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..70578b87c7b 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -470,6 +470,13 @@ 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 +3294,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 +3463,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 +5488,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 +7088,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 +7979,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 +9024,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);
}
/*
@@ -9209,10 +9269,205 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
convert_value_to_string(estate,
prm->value,
prm->ptype),
- -1);
+ -1);
}
MemoryContextSwitchTo(oldcontext);
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;
+ }
+
+ 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..faa7b46274b 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -435,6 +435,16 @@ typedef struct PLpgSQL_rec
/* We always store record variables as "expanded" records */
ExpandedRecordHeader *erh;
+
+ /*
+ * Snapshot of tupDesc_identifier values for all composite types reachable
+ * from the record's declared type (or, for RECORDOID variables, from the
+ * type adopted from the assigned value). Used to detect mid-transaction
+ * ALTER TYPE. Empty for anonymous rowtypes that cannot be versioned.
+ */
+ 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..0ee7a131eea 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -577,3 +577,124 @@ 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: 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;
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 30+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-23 01:18 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` Re: BUG #19382: Server crash at __nss_database_lookup =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Re: BUG #19382: Server crash at __nss_database_lookup Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-07 23:20 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-13 20:53 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
2026-07-13 22:18 ` Re: BUG #19382: Server crash at __nss_database_lookup Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-15 22:16 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <suryapoondla4@gmail.com>
@ 2026-07-16 06:23 ` Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 0 replies; 30+ messages in thread
From: Zsolt Parragi @ 2026-07-16 06:23 UTC (permalink / raw)
To: surya poondla <suryapoondla4@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org
> the other needs more design work than a small amendment.
I agree that one seems more difficult, I'll also think about it but so
far I don't have a better idea than what you suggested.
> I'd rather propose a design for this separately, likely a session-scoped
> set of "types altered in this transaction" plus
> a check at composite-read time. Happy to open a fresh thread on it.
That's fine with me if others agree with the approach, the current
patch is a clear improvement over the current behavior.
Case 2 is the only remaining issue I can reproduce currently, I can
crash the server with multiple variations with it, but those all
follow the same generic idea.
^ permalink raw reply [nested|flat] 30+ messages in thread
end of thread, other threads:[~2026-07-16 06:23 UTC | newest]
Thread overview: 30+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-20 05:02 BUG #19382: Server crash at __nss_database_lookup PG Bug reporting form <noreply@postgresql.org>
2026-01-20 09:15 ` Kirill Reshke <reshkekirill@gmail.com>
2026-01-20 09:25 ` Kirill Reshke <reshkekirill@gmail.com>
2026-01-20 09:47 ` Kirill Reshke <reshkekirill@gmail.com>
2026-01-23 01:18 ` surya poondla <suryapoondla4@gmail.com>
2026-01-24 01:55 ` surya poondla <suryapoondla4@gmail.com>
2026-01-26 22:04 ` surya poondla <suryapoondla4@gmail.com>
2026-02-02 21:45 ` surya poondla <suryapoondla4@gmail.com>
2026-02-26 23:55 ` surya poondla <suryapoondla4@gmail.com>
2026-03-16 22:09 ` surya poondla <suryapoondla4@gmail.com>
2026-03-19 05:00 ` surya poondla <suryapoondla4@gmail.com>
2026-03-19 07:53 ` =?utf-8?B?c29uZ2ppbnpob3U=?= <tsinghualucky912@foxmail.com>
2026-03-20 18:16 ` surya poondla <suryapoondla4@gmail.com>
2026-04-02 11:18 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-02 23:14 ` surya poondla <suryapoondla4@gmail.com>
2026-04-04 12:42 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-09 04:24 ` surya poondla <suryapoondla4@gmail.com>
2026-04-14 23:49 ` surya poondla <suryapoondla4@gmail.com>
2026-04-16 12:00 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-16 23:20 ` surya poondla <suryapoondla4@gmail.com>
2026-04-17 06:57 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-04-23 04:17 ` surya poondla <suryapoondla4@gmail.com>
2026-06-02 23:44 ` surya poondla <suryapoondla4@gmail.com>
2026-06-30 20:50 ` surya poondla <suryapoondla4@gmail.com>
2026-07-06 22:16 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-07 23:20 ` surya poondla <suryapoondla4@gmail.com>
2026-07-13 20:53 ` surya poondla <suryapoondla4@gmail.com>
2026-07-13 22:18 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-15 22:16 ` surya poondla <suryapoondla4@gmail.com>
2026-07-16 06:23 ` Zsolt Parragi <zsolt.parragi@percona.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox