From 7f7de57106fcf82445f180d152e241c56faaad2b Mon Sep 17 00:00:00 2001
From: Xnerv Wang <xnerv@amazon.com>
Date: Thu, 22 Oct 2026 22:45:56 +0000
Subject: [PATCH 1/2] Add test for use-after-free when flattening expanded
 records

A PL/pgSQL expression that references both a TOASTed field of an
expanded record and the whole record could use the field value after
ER_get_flat_size() had freed it.  Test only; the fix is in a separate
patch.
---
 src/pl/plpgsql/src/expected/plpgsql_record.out | 18 ++++++++++++++++++
 src/pl/plpgsql/src/sql/plpgsql_record.sql      | 16 ++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out
index e5de7143606..c209f803726 100644
--- a/src/pl/plpgsql/src/expected/plpgsql_record.out
+++ b/src/pl/plpgsql/src/expected/plpgsql_record.out
@@ -871,6 +871,24 @@ NOTICE:  processing row 2
 NOTICE:  processing row 3
 ERROR:  value for domain ordered_texts violates check constraint "ordered_texts_check"
 CONTEXT:  PL/pgSQL function inline_code_block line 8 at assignment
+-- check using a toasted field and the whole record in one expression
+-- the dropped column makes "select into" store the fields separately
+create table toasted_hole (id int, junk text, b text);
+alter table toasted_hole alter column b set storage external;
+insert into toasted_hole values (1, 'x', repeat('ffoob',4000));
+alter table toasted_hole drop column junk;
+create function toasted_hole_test() returns bool language plpgsql as
+$$ declare r toasted_hole;
+begin
+  select * into r from toasted_hole;
+  return r.b = (to_jsonb(r) ->> 'b');
+end $$;
+select toasted_hole_test();
+ toasted_hole_test 
+-------------------
+ t
+(1 row)
+
 -- check coercion of a record result to named-composite function output type
 create function compresult(int8) returns two_int8s language plpgsql as
 $$ declare r record; begin r := row($1,$1); return r; end $$;
diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql
index 4fbed38b8bb..acda16babdf 100644
--- a/src/pl/plpgsql/src/sql/plpgsql_record.sql
+++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql
@@ -568,6 +568,22 @@ begin
   end loop;
 end$$;
 
+-- check using a toasted field and the whole record in one expression
+-- the dropped column makes "select into" store the fields separately
+create table toasted_hole (id int, junk text, b text);
+alter table toasted_hole alter column b set storage external;
+insert into toasted_hole values (1, 'x', repeat('ffoob',4000));
+alter table toasted_hole drop column junk;
+
+create function toasted_hole_test() returns bool language plpgsql as
+$$ declare r toasted_hole;
+begin
+  select * into r from toasted_hole;
+  return r.b = (to_jsonb(r) ->> 'b');
+end $$;
+
+select toasted_hole_test();
+
 -- check coercion of a record result to named-composite function output type
 create function compresult(int8) returns two_int8s language plpgsql as
 $$ declare r record; begin r := row($1,$1); return r; end $$;
-- 
2.47.3

