From 661095c40c0bcbb9c49743f518417a2977b63aef Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 11 May 2026 05:13:47 -0700 Subject: [PATCH] Fix MCV input array checks in statistics restore functions The SQL functions for the restore of attribute and expression statistics accept "most_common_vals" and "most_common_freqs" as independent arrays. The planner assumes these have the same number of elements, but it was possible to insert in the catalogs data that would cause an over-read when the catalog data is loaded in the planner. There were two holes in the stats restore logic: - Both arrays should match in size. - The input array must be one-dimensional, and it should match with what is delivered by pg_dump when scanning the pg_stats catalogs. The multivariate extended statistics MCV path (import_mcv) already validated these inputs via check_mcvlist_array(), and is not affected. These problems exist in v18 and newer versions for the restore of attribute statistics. These problems affect only HEAD for the restore of the expression statistics. Reported-by: Jeroen Gui Author: Michael Paquier Reviewed-by: Amit Langote Reviewed-by: John Naylor Security: CVE-2026-6575 Backpatch-through: 18 --- src/backend/statistics/attribute_stats.c | 34 +++++++-- src/test/regress/expected/stats_import.out | 83 +++++++++++++++++++++- src/test/regress/sql/stats_import.sql | 54 ++++++++++++++ 3 files changed, 166 insertions(+), 5 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 38cedaa3263..5c1c5749ad4 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -384,10 +384,27 @@ attribute_statistics_update(FunctionCallInfo fcinfo) if (converted) { - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_MCV, - eq_opr, atttypcoll, - stanumbers, false, stavalues, false); + ArrayType *vals_arr = DatumGetArrayTypeP(stavalues); + ArrayType *nums_arr = DatumGetArrayTypeP(stanumbers); + int nvals = ARR_DIMS(vals_arr)[0]; + int nnums = ARR_DIMS(nums_arr)[0]; + + if (nvals != nnums) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": incorrect number of elements (same as \"%s\" required)", + "most_common_vals", + "most_common_freqs"))); + result = false; + } + else + { + set_stats_slot(values, nulls, replaces, + STATISTIC_KIND_MCV, + eq_opr, atttypcoll, + stanumbers, false, stavalues, false); + } } else result = false; @@ -731,6 +748,15 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid, return (Datum) 0; } + if (ARR_NDIM(DatumGetArrayTypeP(result)) != 1) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" must be a one-dimensional array", staname))); + *ok = false; + return (Datum) 0; + } + if (array_contains_nulls(DatumGetArrayTypeP(result))) { ereport(WARNING, diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 98ce7dc2841..2efd422df16 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -625,6 +625,87 @@ AND attname = 'id'; stats_import | test | id | f | 0.23 | 5 | 0.6 | | | | | | | | | | (1 row) +-- warn: mcv / mcf array length mismatch (more vals), mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.24::real, + 'most_common_vals', '{2,1,3}'::text, + 'most_common_freqs', '{0.3,0.25}'::real[] + ); +WARNING: could not parse "most_common_vals": incorrect number of elements (same as "most_common_freqs" required) + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT * +FROM pg_stats +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram +--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------ + stats_import | test | id | f | 0.24 | 5 | 0.6 | | | | | | | | | | +(1 row) + +-- warn: mcv / mcf array length mismatch (more freqs), mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.25::real, + 'most_common_vals', '{2,1}'::text, + 'most_common_freqs', '{0.3,0.25,0.05}'::real[] + ); +WARNING: could not parse "most_common_vals": incorrect number of elements (same as "most_common_freqs" required) + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT * +FROM pg_stats +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram +--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------ + stats_import | test | id | f | 0.25 | 5 | 0.6 | | | | | | | | | | +(1 row) + +-- warn: most_common_vals is multi-dimensional, mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.26::real, + 'most_common_vals', '{{2,1},{3,4}}'::text, + 'most_common_freqs', '{0.3,0.25,0.05,0.04}'::real[] + ); +WARNING: "most_common_vals" must be a one-dimensional array + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT * +FROM pg_stats +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram +--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------ + stats_import | test | id | f | 0.26 | 5 | 0.6 | | | | | | | | | | +(1 row) + -- ok: mcv+mcf SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -647,7 +728,7 @@ AND inherited = false AND attname = 'id'; schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------ - stats_import | test | id | f | 0.23 | 5 | 0.6 | {2,1,3} | {0.3,0.25,0.05} | | | | | | | | + stats_import | test | id | f | 0.26 | 5 | 0.6 | {2,1,3} | {0.3,0.25,0.05} | | | | | | | | (1 row) -- warn: NULL in histogram array, rest get set diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index d140733a750..ee97fa6bc1b 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -457,6 +457,60 @@ AND tablename = 'test' AND inherited = false AND attname = 'id'; +-- warn: mcv / mcf array length mismatch (more vals), mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.24::real, + 'most_common_vals', '{2,1,3}'::text, + 'most_common_freqs', '{0.3,0.25}'::real[] + ); + +SELECT * +FROM pg_stats +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + +-- warn: mcv / mcf array length mismatch (more freqs), mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.25::real, + 'most_common_vals', '{2,1}'::text, + 'most_common_freqs', '{0.3,0.25,0.05}'::real[] + ); + +SELECT * +FROM pg_stats +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + +-- warn: most_common_vals is multi-dimensional, mcv-pair fails, rest get set +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 0.26::real, + 'most_common_vals', '{{2,1},{3,4}}'::text, + 'most_common_freqs', '{0.3,0.25,0.05,0.04}'::real[] + ); + +SELECT * +FROM pg_stats +WHERE schemaname = 'stats_import' +AND tablename = 'test' +AND inherited = false +AND attname = 'id'; + -- ok: mcv+mcf SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import',