From 3517bef1548beb94b9d5fb0f91e63f1d63c74a03 Mon Sep 17 00:00:00 2001 From: Xnerv Wang Date: Thu, 22 Oct 2026 22:45:56 +0000 Subject: [PATCH 2/2] Fix use-after-free when flattening expanded records ER_get_flat_size() detoasts out-of-line fields before flattening a record, and replacing a field frees the old value if it isn't part of the record's flat tuple. But a caller may still be using a field value it fetched earlier, as in a PL/pgSQL expression that references both a field and the whole record. So ER_get_flat_size() now leaves the old value alone; ordinary field assignments still free it. --- src/backend/utils/adt/expandedrecord.c | 41 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/backend/utils/adt/expandedrecord.c b/src/backend/utils/adt/expandedrecord.c index 13752db44e8..c0ce52649b2 100644 --- a/src/backend/utils/adt/expandedrecord.c +++ b/src/backend/utils/adt/expandedrecord.c @@ -45,6 +45,12 @@ static const ExpandedObjectMethods ER_methods = static void ER_mc_callback(void *arg); static MemoryContext get_short_term_cxt(ExpandedRecordHeader *erh); static void build_dummy_expanded_header(ExpandedRecordHeader *main_erh); +static void expanded_record_set_field_worker(ExpandedRecordHeader *erh, + int fnumber, + Datum newValue, bool isnull, + bool expand_external, + bool check_constraints, + bool free_old_value); static pg_noinline void check_domain_for_new_field(ExpandedRecordHeader *erh, int fnumber, Datum newValue, bool isnull); @@ -706,13 +712,19 @@ ER_get_flat_size(ExpandedObjectHeader *eohptr) VARATT_IS_EXTERNAL(DatumGetPointer(erh->dvalues[i]))) { /* - * expanded_record_set_field_internal can do the actual work - * of detoasting. It needn't recheck domain constraints. + * expanded_record_set_field_worker can do the actual work of + * detoasting. It needn't recheck domain constraints. + * + * It mustn't pfree the old value, though: a caller may still + * be using a field value it fetched earlier. The old value + * is just a TOAST pointer, so it can stay around until the + * record is deleted. */ - expanded_record_set_field_internal(erh, i + 1, - erh->dvalues[i], false, - true, - false); + expanded_record_set_field_worker(erh, i + 1, + erh->dvalues[i], false, + true, + false, + false); } } @@ -1113,6 +1125,21 @@ expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber, Datum newValue, bool isnull, bool expand_external, bool check_constraints) +{ + expanded_record_set_field_worker(erh, fnumber, newValue, isnull, + expand_external, check_constraints, + true); +} + +/* + * Workhorse for expanded_record_set_field_internal + */ +static void +expanded_record_set_field_worker(ExpandedRecordHeader *erh, int fnumber, + Datum newValue, bool isnull, + bool expand_external, + bool check_constraints, + bool free_old_value) { TupleDesc tupdesc; CompactAttribute *attr; @@ -1221,7 +1248,7 @@ expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber, * whatever we just allocated here is in the short-lived domain check * context. */ - if (oldValue && !(erh->flags & ER_FLAG_IS_DUMMY)) + if (oldValue && free_old_value && !(erh->flags & ER_FLAG_IS_DUMMY)) { /* Don't try to pfree a part of the original flat record */ if (oldValue < erh->fstartptr || oldValue >= erh->fendptr) -- 2.47.3