public inbox for [email protected]
help / color / mirror / Atom feedRe: BUG #19382: Server crash at __nss_database_lookup
6+ messages / 1 participants
[nested] [flat]
* Re: BUG #19382: Server crash at __nss_database_lookup
@ 2026-01-23 01:18 surya poondla <[email protected]>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
0 siblings, 1 reply; 6+ messages in thread
From: surya poondla @ 2026-01-23 01:18 UTC (permalink / raw)
To: [email protected]; [email protected]
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] 6+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-23 01:18 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
@ 2026-01-24 01:55 ` surya poondla <[email protected]>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
0 siblings, 1 reply; 6+ messages in thread
From: surya poondla @ 2026-01-24 01:55 UTC (permalink / raw)
To: [email protected]; [email protected]
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, 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 <[email protected]>
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] 6+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-23 01:18 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
@ 2026-01-26 22:04 ` surya poondla <[email protected]>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
0 siblings, 1 reply; 6+ messages in thread
From: surya poondla @ 2026-01-26 22:04 UTC (permalink / raw)
To: [email protected]; [email protected]
Hi All,
I also tested the patch on postgres 19, it shows the same working behavior
as postgres 17.
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-23 01:18 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
@ 2026-02-02 21:45 ` surya poondla <[email protected]>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
0 siblings, 1 reply; 6+ messages in thread
From: surya poondla @ 2026-02-02 21:45 UTC (permalink / raw)
To: [email protected]; [email protected]
Hi All,
I created a commitfest entry here at:
https://commitfest.postgresql.org/patch/6449/
>
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-23 01:18 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
@ 2026-02-26 23:55 ` surya poondla <[email protected]>
2026-03-16 22:09 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
0 siblings, 1 reply; 6+ messages in thread
From: surya poondla @ 2026-02-26 23:55 UTC (permalink / raw)
To: [email protected]; [email protected]
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, 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 <[email protected]>
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] 6+ messages in thread
* Re: BUG #19382: Server crash at __nss_database_lookup
2026-01-23 01:18 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-24 01:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-26 22:04 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-02-02 21:45 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-02-26 23:55 ` Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
@ 2026-03-16 22:09 ` surya poondla <[email protected]>
0 siblings, 0 replies; 6+ messages in thread
From: surya poondla @ 2026-03-16 22:09 UTC (permalink / raw)
To: [email protected]; [email protected]
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, 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 <[email protected]>
Date: Fri, 23 Jan 2026 17:28:54 -0800
Subject: [PATCH v3] Fix (bug #19382) server crash when ALTER TYPE is used
mid-transaction in PL/pgSQL
When ALTER TYPE changes a composite type's column types within a
transaction, PL/pgSQL record variables that were populated before
the ALTER still hold data in the old format. Returning such records
causes a crash because the output functions expect data matching the
new type definition, not the old one.
The crash manifested as a segmentation fault in record_out() when it
attempted to interpret integer data as a text pointer, due to the
mismatch between the stored data and the current type definition.
---
src/pl/plpgsql/src/pl_exec.c | 173 ++++++++++++++++++++++++++++++++++-
1 file changed, 172 insertions(+), 1 deletion(-)
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 6b077febdc8..0d85a95d795 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -458,6 +458,8 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
static PLpgSQL_variable *make_callstmt_target(PLpgSQL_execstate *estate,
PLpgSQL_expr *expr);
+static void convert_record_for_altered_type(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec);
/* ----------
* plpgsql_exec_function Called by the call handler for
@@ -3244,8 +3246,30 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt)
}
break;
- case PLPGSQL_DTYPE_ROW:
case PLPGSQL_DTYPE_REC:
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
+ int32 rettypmod;
+
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, convert the data to
+ * prevent crashes when outputting the record.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL &&
+ !ExpandedRecordIsEmpty(rec->erh))
+ convert_record_for_altered_type(estate, rec);
+
+ exec_eval_datum(estate,
+ retvar,
+ &estate->rettype,
+ &rettypmod,
+ &estate->retval,
+ &estate->retisnull);
+ }
+ break;
+
+ case PLPGSQL_DTYPE_ROW:
{
/* exec_eval_datum can handle these cases */
int32 rettypmod;
@@ -3390,6 +3414,14 @@ exec_stmt_return_next(PLpgSQL_execstate *estate,
TupleDesc rec_tupdesc;
TupleConversionMap *tupmap;
+ /*
+ * Check if the record's composite type was altered since
+ * the record was populated. If so, convert the data to
+ * prevent crashes when storing to the tuplestore.
+ */
+ if (rec->rectypeid != RECORDOID && rec->erh != NULL)
+ convert_record_for_altered_type(estate, rec);
+
/* If rec is null, try to convert it to a row of nulls */
if (rec->erh == NULL)
instantiate_empty_record_variable(estate, rec);
@@ -8883,3 +8915,142 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
return paramstr.data;
}
+
+/*
+ * convert_record_for_altered_type
+ *
+ * Check if a record's composite type has been altered since the record
+ * was populated, and if so, convert the record data to match the new
+ * type definition. This prevents crashes that can occur when the stored
+ * data doesn't match the current type definition.
+ *
+ * If conversion is needed, assigns the new record to rec via
+ * assign_record_var(), which transfers it to datum_context and frees
+ * the old record.
+ */
+static void
+convert_record_for_altered_type(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec)
+{
+ ExpandedRecordHeader *erh = rec->erh;
+ Oid rectypeid = rec->rectypeid;
+ TupleDesc old_tupdesc;
+ TupleDesc new_tupdesc;
+ TypeCacheEntry *typentry;
+ uint64 current_tupdesc_id;
+ ExpandedRecordHeader *new_erh;
+ Datum *old_values;
+ bool *old_nulls;
+ Datum *new_values;
+ bool *new_nulls;
+ int natts;
+ int i;
+ MemoryContext oldcxt;
+ bool need_conversion = false;
+
+ /* Nothing to do for anonymous RECORD type */
+ if (rectypeid == RECORDOID)
+ return;
+
+ /* Get current type definition from typcache */
+ typentry = lookup_type_cache(rectypeid,
+ TYPECACHE_TUPDESC |
+ TYPECACHE_DOMAIN_BASE_INFO);
+ if (typentry->typtype == TYPTYPE_DOMAIN)
+ typentry = lookup_type_cache(typentry->domainBaseType,
+ TYPECACHE_TUPDESC);
+
+ current_tupdesc_id = typentry->tupDesc_identifier;
+
+ /* If type hasn't changed, nothing to do (fast path) */
+ if (erh->er_tupdesc_id == current_tupdesc_id)
+ return;
+
+ /*
+ * Type version has changed. Need to check if field types actually differ
+ * and convert if necessary.
+ */
+ old_tupdesc = erh->er_tupdesc;
+ new_tupdesc = typentry->tupDesc;
+
+ /* Sanity check: must have same number of attributes */
+ if (old_tupdesc->natts != new_tupdesc->natts)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("record type \"%s\" structure has changed",
+ format_type_be(rectypeid)),
+ errdetail("Number of columns changed from %d to %d.",
+ old_tupdesc->natts, new_tupdesc->natts)));
+
+ natts = old_tupdesc->natts;
+
+ /* Deconstruct the old record to access field values */
+ deconstruct_expanded_record(erh);
+ old_values = erh->dvalues;
+ old_nulls = erh->dnulls;
+
+ /* Allocate arrays for new values */
+ oldcxt = MemoryContextSwitchTo(get_eval_mcontext(estate));
+ new_values = (Datum *) palloc(natts * sizeof(Datum));
+ new_nulls = (bool *) palloc(natts * sizeof(bool));
+ MemoryContextSwitchTo(oldcxt);
+
+ /* Convert each field */
+ for (i = 0; i < natts; i++)
+ {
+ Form_pg_attribute old_att = TupleDescAttr(old_tupdesc, i);
+ Form_pg_attribute new_att = TupleDescAttr(new_tupdesc, i);
+
+ /* Skip dropped columns */
+ if (old_att->attisdropped || new_att->attisdropped)
+ {
+ new_values[i] = (Datum) 0;
+ new_nulls[i] = true;
+ continue;
+ }
+
+ /* If null, stays null */
+ if (old_nulls[i])
+ {
+ new_values[i] = (Datum) 0;
+ new_nulls[i] = true;
+ continue;
+ }
+
+ /* If same type, no conversion needed */
+ if (old_att->atttypid == new_att->atttypid &&
+ (old_att->atttypmod == new_att->atttypmod ||
+ new_att->atttypmod == -1))
+ {
+ new_values[i] = old_values[i];
+ new_nulls[i] = false;
+ continue;
+ }
+
+ /* Different type: convert using exec_cast_value */
+ need_conversion = true;
+ new_nulls[i] = false;
+ new_values[i] = exec_cast_value(estate,
+ old_values[i],
+ &new_nulls[i],
+ old_att->atttypid,
+ old_att->atttypmod,
+ new_att->atttypid,
+ new_att->atttypmod);
+ }
+
+ /* If no actual conversion was needed, return without modifying rec */
+ if (!need_conversion)
+ return;
+
+ /* Build new expanded record with converted values */
+ new_erh = make_expanded_record_from_typeid(rectypeid, -1,
+ get_eval_mcontext(estate));
+ expanded_record_set_fields(new_erh, new_values, new_nulls, true);
+
+ /*
+ * Assign the new record to rec, transferring it to datum_context
+ * and freeing the old record.
+ */
+ assign_record_var(estate, rec, new_erh);
+}
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 6+ messages in thread
end of thread, other threads:[~2026-03-16 22:09 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-23 01:18 Re: BUG #19382: Server crash at __nss_database_lookup surya poondla <[email protected]>
2026-01-24 01:55 ` surya poondla <[email protected]>
2026-01-26 22:04 ` surya poondla <[email protected]>
2026-02-02 21:45 ` surya poondla <[email protected]>
2026-02-26 23:55 ` surya poondla <[email protected]>
2026-03-16 22:09 ` surya poondla <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox