From 7f7de57106fcf82445f180d152e241c56faaad2b Mon Sep 17 00:00:00 2001 From: Xnerv Wang 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