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 <jeroen.gui1@proton.me>
Author: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: John Naylor <johncnaylorls@gmail.com>
Security: CVE-2026-6575
Backpatch-through: 18
This commit is contained in:
Michael Paquier 2026-05-11 05:13:46 -07:00 committed by Noah Misch
parent ec8ded4b32
commit 6d6348f032
5 changed files with 248 additions and 5 deletions

View file

@ -373,10 +373,27 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
if (converted)
{
statatt_set_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
{
statatt_set_slot(values, nulls, replaces,
STATISTIC_KIND_MCV,
eq_opr, atttypcoll,
stanumbers, false, stavalues, false);
}
}
else
result = false;

View file

@ -1070,6 +1070,15 @@ array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod,
return (Datum) 0;
}
if (ARR_NDIM(DatumGetArrayTypeP(result)) != 1)
{
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("could not import element \"%s\" in expression %d: must be a one-dimensional array",
element_name, exprnum)));
return (Datum) 0;
}
if (array_contains_nulls(DatumGetArrayTypeP(result)))
{
ereport(WARNING,
@ -1332,10 +1341,27 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
/* Only set the slot if both datums have been built */
if (val_ok && num_ok)
{
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")));
goto pg_statistic_error;
}
statatt_set_slot(values, nulls, replaces,
STATISTIC_KIND_MCV,
typcache->eq_opr, typcoll,
stanumbers, false, stavalues, false);
}
else
goto pg_statistic_error;
}

View file

@ -600,6 +600,15 @@ statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid ty
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,

View file

@ -824,6 +824,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 stats_import.pg_stats_stable
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 stats_import.pg_stats_stable
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 stats_import.pg_stats_stable
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',
@ -846,7 +927,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
@ -2524,6 +2605,23 @@ HINT: "most_common_vals" and "most_common_freqs" must be both either strings or
f
(1 row)
-- exprs most_common_vals is multi-dimensional
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
'relname', 'test_clone',
'statistics_schemaname', 'stats_import',
'statistics_name', 'test_stat_clone',
'inherited', false,
'exprs', '[
{ "most_common_vals": "{{1,2},{3,4}}", "most_common_freqs": "{0.3,0.25,0.05,0.04}" },
{ "most_common_vals": "{2}", "most_common_freqs": "{0.5}" }
]'::jsonb);
WARNING: could not import element "most_common_vals" in expression -1: must be a one-dimensional array
pg_restore_extended_stats
---------------------------
f
(1 row)
-- exprs most_common_vals element wrong type
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
@ -2582,6 +2680,23 @@ HINT: Element "most_common_freqs" in expression -1 could not be parsed.
f
(1 row)
-- exprs most_common_vals / most_common_freqs array length mismatch
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
'relname', 'test_clone',
'statistics_schemaname', 'stats_import',
'statistics_name', 'test_stat_clone',
'inherited', false,
'exprs', '[
{ "most_common_vals": "{1,3}", "most_common_freqs": "{0.5}" },
{ "most_common_vals": "{2}", "most_common_freqs": "{0.5}" }
]'::jsonb);
WARNING: could not parse "most_common_vals": incorrect number of elements (same as "most_common_freqs" required)
pg_restore_extended_stats
---------------------------
f
(1 row)
-- exprs histogram wrong type
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',

View file

@ -645,6 +645,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 stats_import.pg_stats_stable
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 stats_import.pg_stats_stable
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 stats_import.pg_stats_stable
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',
@ -1784,6 +1838,17 @@ SELECT pg_catalog.pg_restore_extended_stats(
{ "most_common_freqs": "{0.5}" },
{ "most_common_vals": "{2}", "most_common_freqs": "{0.5}" }
]'::jsonb);
-- exprs most_common_vals is multi-dimensional
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
'relname', 'test_clone',
'statistics_schemaname', 'stats_import',
'statistics_name', 'test_stat_clone',
'inherited', false,
'exprs', '[
{ "most_common_vals": "{{1,2},{3,4}}", "most_common_freqs": "{0.3,0.25,0.05,0.04}" },
{ "most_common_vals": "{2}", "most_common_freqs": "{0.5}" }
]'::jsonb);
-- exprs most_common_vals element wrong type
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
@ -1828,6 +1893,17 @@ SELECT pg_catalog.pg_restore_extended_stats(
{ "most_common_vals": "{1}", "most_common_freqs": "{BADMCF}" },
{ "most_common_vals": "{2}", "most_common_freqs": "{0.5}" }
]'::jsonb);
-- exprs most_common_vals / most_common_freqs array length mismatch
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
'relname', 'test_clone',
'statistics_schemaname', 'stats_import',
'statistics_name', 'test_stat_clone',
'inherited', false,
'exprs', '[
{ "most_common_vals": "{1,3}", "most_common_freqs": "{0.5}" },
{ "most_common_vals": "{2}", "most_common_freqs": "{0.5}" }
]'::jsonb);
-- exprs histogram wrong type
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',