From 08454e8b2defdf8a0b61e550606f78b8f184f2e6 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 10 Aug 2026 06:38:12 -0700 Subject: [PATCH] Fix multirange type handling in pg_restore_attribute_stats() statatt_get_type() unconditionally converted multirange types to their underlying range type. This choice affected all the type information, like atttypid, atttyptype and operators. This made the bounds histogram work correctly (range type is required), but it was wrong for all the other stat kinds. MCV values for a multirange column should be parsed as multirange arrays, not range arrays. It also made the TYPTYPE_MULTIRANGE check for the range stats validation as dead code, since atttyptype was always TYPTYPE_RANGE after the conversion due to the centralized statatt_get_type(). pg_restore_extended_stats() handles the same case correctly: it keeps the original type and explicitly converts to the range type only at the point where range_histogram_bounds is built. The fix of this issue is simple: the multirange-to-range conversion needs to be moved from the centralized statatt_get_type() up to where attribute stats build their range_histogram_bounds, matching what is done for extended statistics restore. The regression tests for multiranges with attribute stats are extended to cover this case. Author: OpenAI Security Research Team Backpatch-through: 18 Security: CVE-2026-16238 --- src/backend/statistics/attribute_stats.c | 17 +++++++++-------- src/test/regress/expected/stats_import.out | 22 +++++++++++++++++++++- src/test/regress/sql/stats_import.sql | 16 +++++++++++++++- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 357466456ff..50f89ff0ac3 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -492,11 +492,19 @@ attribute_statistics_update(FunctionCallInfo fcinfo) { bool converted = false; Datum stavalues; + Oid bounds_typid = atttypid; + + /* + * If it's a multirange, step down to the range type, as is done by + * multirange_typanalyze(). + */ + if (type_is_multirange(atttypid)) + bounds_typid = get_multirange_range(atttypid); stavalues = text_to_stavalues("range_bounds_histogram", &array_in_fn, PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG), - atttypid, atttypmod, + bounds_typid, atttypmod, &converted); if (converted) @@ -647,13 +655,6 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, } ReleaseSysCache(atup); - /* - * If it's a multirange, step down to the range type, as is done by - * multirange_typanalyze(). - */ - if (type_is_multirange(*atttypid)) - *atttypid = get_multirange_range(*atttypid); - /* finds the right operators even if atttypid is a domain */ typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR); *atttyptype = typcache->typtype; diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 8a19a398e28..78e5f4563fb 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -1238,12 +1238,32 @@ VALUES (1, 'red', '{[1,3),[5,9),[20,30)}'::int4multirange), (2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange), (3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange); --- ensure that we set attribute stats for a multirange +-- warn: reject range values as ordinary multirange statistics SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', 'relname', 'test_mr', 'attname', 'mrange', 'inherited', false, + 'most_common_vals', ARRAY['[1,3)']::text, + 'most_common_freqs', ARRAY[1.0]::real[] +); +WARNING: malformed multirange literal: "[1,3)" +DETAIL: Missing left brace. + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +-- ensure that we set attribute stats for a multirange +-- MCVs and histograms retain the multirange type. +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'attname', 'mrange', + 'inherited', false, + 'most_common_vals', ARRAY['{[1,3),[5,9)}', '{[11,13),[15,19)}']::text, + 'most_common_freqs', ARRAY[0.6, 0.4]::real[], + 'histogram_bounds', ARRAY['{[1,3)}', '{[11,13)}', '{[21,23)}']::text, 'range_length_histogram', '{19,29,109}'::text, 'range_empty_frac', '0'::real, 'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index e799aebf1f8..0ba5791e2e5 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -857,12 +857,26 @@ VALUES (2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange), (3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange); --- ensure that we set attribute stats for a multirange +-- warn: reject range values as ordinary multirange statistics SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', 'relname', 'test_mr', 'attname', 'mrange', 'inherited', false, + 'most_common_vals', ARRAY['[1,3)']::text, + 'most_common_freqs', ARRAY[1.0]::real[] +); + +-- ensure that we set attribute stats for a multirange +-- MCVs and histograms retain the multirange type. +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'attname', 'mrange', + 'inherited', false, + 'most_common_vals', ARRAY['{[1,3),[5,9)}', '{[11,13),[15,19)}']::text, + 'most_common_freqs', ARRAY[0.6, 0.4]::real[], + 'histogram_bounds', ARRAY['{[1,3)}', '{[11,13)}', '{[21,23)}']::text, 'range_length_histogram', '{19,29,109}'::text, 'range_empty_frac', '0'::real, 'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text