v1-0001-Reject-oversized-MCV-lists-in-extended-statistics.patch

application/octet-stream

Filename: v1-0001-Reject-oversized-MCV-lists-in-extended-statistics.patch
Type: application/octet-stream
Part: 0
Message: [PATCH] pg_restore_extended_stats() can store an MCV list that cannot be read back

Patch

Format: format-patch
Series: patch v1-0001
Subject: Reject oversized MCV lists in extended statistics import
File+
src/backend/statistics/extended_stats_funcs.c 17 0
src/test/regress/expected/stats_import.out 20 0
src/test/regress/sql/stats_import.sql 15 0
From 3cdef8541488d46027cc92f6f688f3bddc73e722 Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Tue, 16 Jun 2026 18:16:26 +0800
Subject: [PATCH v1] Reject oversized MCV lists in extended statistics import

import_mcv(), reached from pg_restore_extended_stats(), stored a
most-common-values list of any length, but the planner and
statext_mcv_deserialize() reject a list with more than
STATS_MCVLIST_MAX_ITEMS items.  An oversized list could therefore be
imported successfully and then make every attempt to read the statistics
object back fail with "invalid length (%u) item array in MCVList",
leaving the affected columns unusable for planning until the bad
statistics are removed.

Reject such a list at import time with a WARNING, matching how the other
most_common_vals array checks report invalid input, so the oversized
list is never stored.
---
 src/backend/statistics/extended_stats_funcs.c | 17 ++++++++++++++++
 src/test/regress/expected/stats_import.out    | 20 +++++++++++++++++++
 src/test/regress/sql/stats_import.sql         | 15 ++++++++++++++
 3 files changed, 52 insertions(+)

diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 4a65a46df41..db88a0adeb7 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -851,6 +851,23 @@ import_mcv(const ArrayType *mcv_arr, const ArrayType *freqs_arr,
 	 * the reference array for determining their length.
 	 */
 	nitems = ARR_DIMS(mcv_arr)[0];
+
+	/*
+	 * Reject a list larger than the maximum the planner and the MCV
+	 * deserialization code accept (STATS_MCVLIST_MAX_ITEMS).  Without this
+	 * check an oversized list would be stored successfully but then make every
+	 * attempt to read it back fail in statext_mcv_deserialize().
+	 */
+	if (nitems > STATS_MCVLIST_MAX_ITEMS)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("could not parse array \"%s\": number of items (%d) exceeds maximum (%d)",
+					   extarginfo[MOST_COMMON_VALS_ARG].argname,
+					   nitems, STATS_MCVLIST_MAX_ITEMS));
+		goto mcv_error;
+	}
+
 	if (!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG, 1, nitems) ||
 		!check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG, 1, nitems))
 	{
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 4520f0b664e..f65ca0fc59f 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -2231,6 +2231,26 @@ WARNING:  could not parse array "most_common_vals": found 4 attributes but expec
  f
 (1 row)
 
+-- warn: more MCV items than the planner/deserialization code can read back
+-- (must be rejected, not stored)
+SELECT pg_catalog.pg_restore_extended_stats(
+  'schemaname', 'stats_import',
+  'relname', 'test',
+  'statistics_schemaname', 'stats_import',
+  'statistics_name', 'test_stat_mcv',
+  'inherited', false,
+  'most_common_vals', (SELECT array_agg(ARRAY[g::text, g::text])
+                       FROM generate_series(1, 10001) g),
+  'most_common_freqs', (SELECT array_agg((1.0 / 10001)::double precision)
+                        FROM generate_series(1, 10001) g),
+  'most_common_base_freqs', (SELECT array_agg((1.0 / 10001)::double precision)
+                             FROM generate_series(1, 10001) g));
+WARNING:  could not parse array "most_common_vals": number of items (10001) exceeds maximum (10000)
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
 -- ok: mcv
 SELECT pg_catalog.pg_restore_extended_stats(
   'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 6064b7722da..a3a2795efc6 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -1612,6 +1612,21 @@ SELECT pg_catalog.pg_restore_extended_stats(
   'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[],
   'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]);
 
+-- warn: more MCV items than the planner/deserialization code can read back
+-- (must be rejected, not stored)
+SELECT pg_catalog.pg_restore_extended_stats(
+  'schemaname', 'stats_import',
+  'relname', 'test',
+  'statistics_schemaname', 'stats_import',
+  'statistics_name', 'test_stat_mcv',
+  'inherited', false,
+  'most_common_vals', (SELECT array_agg(ARRAY[g::text, g::text])
+                       FROM generate_series(1, 10001) g),
+  'most_common_freqs', (SELECT array_agg((1.0 / 10001)::double precision)
+                        FROM generate_series(1, 10001) g),
+  'most_common_base_freqs', (SELECT array_agg((1.0 / 10001)::double precision)
+                             FROM generate_series(1, 10001) g));
+
 -- ok: mcv
 SELECT pg_catalog.pg_restore_extended_stats(
   'schemaname', 'stats_import',
-- 
2.47.3