]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix MCV input array checks in statistics restore functions
authorMichael Paquier <michael@paquier.xyz>
Mon, 11 May 2026 12:13:46 +0000 (05:13 -0700)
committerNoah Misch <noah@leadboat.com>
Mon, 11 May 2026 12:13:46 +0000 (05:13 -0700)
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

src/backend/statistics/attribute_stats.c
src/backend/statistics/extended_stats_funcs.c
src/backend/statistics/stat_utils.c
src/test/regress/expected/stats_import.out
src/test/regress/sql/stats_import.sql

index a6b118a8d721a9f9540079a28cc4a2b1114368a0..1cc4d657231afe840cb83e7520b0de4944d5f14c 100644 (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;
index 8537d9e2409b97d1533bf0a91055c8767554f63e..70393d3a9040fd87193e123a2412d102dbbf1075 100644 (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;
        }
index 9c680f1cb37170c418d4995854a0111218c9e4d2..a673e3c704b25726d50a370753d43a5dabb426ad 100644 (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,
index fb2b22e7e55811817e9ef0b1dcc1a5334b0f4c1c..f421e83e2327099c082bdf2630befc5b69d275d2 100644 (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',
index 0bfa3d44cef3bbc2e4fbbfef4d92190dd5f6a55a..c1bf55690a6bce82648e439a93dfa59361432883 100644 (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',