From 70ea58b8810cd7b781bd07f138879d81fd1d76ed Mon Sep 17 00:00:00 2001 From: Ewan Young Date: Wed, 26 Aug 2026 00:07:55 +0800 Subject: [PATCH] Reject non-finite values when restoring attribute statistics pg_restore_attribute_stats() accepted NaN and Infinity for its float arguments and stored them verbatim in pg_statistic. Statistics produced by ANALYZE are always finite, and the planner does not defend against non-finite ones: CLAMP_PROBABILITY() is a pair of comparisons that are both false for NaN, so a NaN null_frac or n_distinct propagates into selectivity estimates (row counts blow up to the clamp ceiling), and a NaN correlation produces a literal "cost=...NaN" in an index scan, which corrupts path cost comparisons. pg_restore_relation_stats() already rejects a non-finite reltuples (7cb9060dcde); this extends the same treatment to the attribute-stats entry points, which the earlier work did not cover. The scalar arguments null_frac, n_distinct, correlation and range_empty_frac, and the float4[] arguments most_common_freqs, most_common_elem_freqs and elem_count_histogram are checked; a bad value is dropped with a WARNING, as for the other non-fatal checks, and the rest of the import proceeds. Negative n_distinct values, which encode a distinct-value ratio rather than a count, are still accepted, matching the -1.0 special case kept for reltuples. These functions perform only superficial validation by design, so this does not attempt to reject out-of-range finite values (which the planner does clamp); it only rejects the non-finite values the planner cannot. --- src/backend/statistics/attribute_stats.c | 40 +++++++- src/backend/statistics/stat_utils.c | 68 ++++++++++++++ src/include/statistics/stat_utils.h | 4 + src/test/regress/expected/stats_import.out | 101 +++++++++++++++++++++ src/test/regress/sql/stats_import.sql | 62 +++++++++++++ 5 files changed, 272 insertions(+), 3 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 16b0dd6a003..0c8cc2a161f 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -253,18 +253,21 @@ attribute_statistics_update_internal(Oid reloid, * and set the corresponding argument to NULL in fcinfo. */ - if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG)) + if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG) || + !stats_check_arg_array_finite(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG)) { do_mcv = false; result = false; } - if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG)) + if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG) || + !stats_check_arg_array_finite(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG)) { do_mcelem = false; result = false; } - if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG)) + if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG) || + !stats_check_arg_array_finite(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG)) { do_dechist = false; result = false; @@ -293,6 +296,37 @@ attribute_statistics_update_internal(Oid reloid, result = false; } + /* + * Reject non-finite scalar values. Done after the pair check so that + * clearing a bad range_empty_frac does not trip that check. null_frac and + * n_distinct are read via PG_ARGISNULL() at storage time, so nulling the + * argument suffices; correlation and range_empty_frac are gated by the "do" + * flags above, which must be cleared instead. + */ + if (!stats_check_arg_finite(fcinfo, attarginfo, NULL_FRAC_ARG)) + { + fcinfo->args[NULL_FRAC_ARG].isnull = true; + result = false; + } + + if (!stats_check_arg_finite(fcinfo, attarginfo, N_DISTINCT_ARG)) + { + fcinfo->args[N_DISTINCT_ARG].isnull = true; + result = false; + } + + if (!stats_check_arg_finite(fcinfo, attarginfo, CORRELATION_ARG)) + { + do_correlation = false; + result = false; + } + + if (!stats_check_arg_finite(fcinfo, attarginfo, RANGE_EMPTY_FRAC_ARG)) + { + do_range_length_histogram = false; + result = false; + } + /* derive information from attribute */ statatt_get_type(reloid, attnum, &atttypid, &atttypmod, diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c index 5ff37ef4cf8..d76a74bf3ac 100644 --- a/src/backend/statistics/stat_utils.c +++ b/src/backend/statistics/stat_utils.c @@ -16,6 +16,8 @@ #include "postgres.h" +#include + #include "access/htup_details.h" #include "access/relation.h" #include "catalog/index.h" @@ -62,6 +64,72 @@ stats_check_required_arg(FunctionCallInfo fcinfo, arginfo[argnum].argname))); } +/* + * Check that a float argument is either NULL or a finite value. + * + * If a problem is found, emit a WARNING, and return false. Otherwise return + * true. + */ +bool +stats_check_arg_finite(FunctionCallInfo fcinfo, + struct StatsArgInfo *arginfo, + int argnum) +{ + if (PG_ARGISNULL(argnum)) + return true; + + if (!isfinite(DatumGetFloat4(PG_GETARG_DATUM(argnum)))) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("argument \"%s\" must be a finite value", + arginfo[argnum].argname))); + return false; + } + + return true; +} + +/* + * Check that a float4[] argument is either NULL or contains only finite + * values. + * + * If a problem is found, emit a WARNING, and return false. Otherwise return + * true. + */ +bool +stats_check_arg_array_finite(FunctionCallInfo fcinfo, + struct StatsArgInfo *arginfo, + int argnum) +{ + ArrayType *arr; + float4 *elems; + int nelems; + + if (PG_ARGISNULL(argnum)) + return true; + + arr = DatumGetArrayTypeP(PG_GETARG_DATUM(argnum)); + Assert(ARR_ELEMTYPE(arr) == FLOAT4OID); + + elems = (float4 *) ARR_DATA_PTR(arr); + nelems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); + + for (int i = 0; i < nelems; i++) + { + if (!isfinite(elems[i])) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("argument \"%s\" array must not contain non-finite values", + arginfo[argnum].argname))); + return false; + } + } + + return true; +} + /* * Check that argument is either NULL or a one dimensional array with no * NULLs. diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h index 15e962dbb7c..1210cc6fe2c 100644 --- a/src/include/statistics/stat_utils.h +++ b/src/include/statistics/stat_utils.h @@ -30,6 +30,10 @@ extern void stats_check_required_arg(FunctionCallInfo fcinfo, int argnum); extern bool stats_check_arg_array(FunctionCallInfo fcinfo, struct StatsArgInfo *arginfo, int argnum); +extern bool stats_check_arg_finite(FunctionCallInfo fcinfo, + struct StatsArgInfo *arginfo, int argnum); +extern bool stats_check_arg_array_finite(FunctionCallInfo fcinfo, + struct StatsArgInfo *arginfo, int argnum); extern bool stats_check_arg_pair(FunctionCallInfo fcinfo, struct StatsArgInfo *arginfo, int argnum1, int argnum2); diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index f3f915e7ed1..15dfb5a7270 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -718,6 +718,107 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'inherited', NULL::boolean, 'null_frac', 0.1::real); ERROR: argument "inherited" must not be null +-- error: non-finite scalar values are rejected with a WARNING and skipped +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 'NaN'::real); +WARNING: argument "null_frac" must be a finite value + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'n_distinct', 'Infinity'::real); +WARNING: argument "n_distinct" must be a finite value + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'correlation', '-Infinity'::real); +WARNING: argument "correlation" must be a finite value + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'range_empty_frac', 'NaN'::real, + 'range_length_histogram', '{399,499,Infinity}'::text); +WARNING: argument "range_empty_frac" must be a finite value + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +-- error: a non-finite element in any of the float4[] arguments is rejected +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'most_common_vals', '{1,2,3}'::text, + 'most_common_freqs', '{0.1,Infinity,0.2}'::real[]); +WARNING: argument "most_common_freqs" array must not contain non-finite values + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'most_common_elems', '{1,2,3}'::text, + 'most_common_elem_freqs', '{0.1,0.2,NaN}'::real[]); +WARNING: argument "most_common_elem_freqs" array must not contain non-finite values + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'tags', + 'inherited', false::boolean, + 'elem_count_histogram', '{1,1,-Infinity,1}'::real[]); +WARNING: argument "elem_count_histogram" array must not contain non-finite values + pg_restore_attribute_stats +---------------------------- + f +(1 row) + +-- ok: a negative n_distinct is a ratio, not a count, and is still accepted +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'n_distinct', '-0.5'::real); + pg_restore_attribute_stats +---------------------------- + t +(1 row) + -- ok: just the fixed values, with version, no stakinds SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index 334d7d8dc00..80268784170 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -565,6 +565,68 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'inherited', NULL::boolean, 'null_frac', 0.1::real); +-- error: non-finite scalar values are rejected with a WARNING and skipped +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'null_frac', 'NaN'::real); + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'n_distinct', 'Infinity'::real); + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'correlation', '-Infinity'::real); + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'range_empty_frac', 'NaN'::real, + 'range_length_histogram', '{399,499,Infinity}'::text); + +-- error: a non-finite element in any of the float4[] arguments is rejected +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'most_common_vals', '{1,2,3}'::text, + 'most_common_freqs', '{0.1,Infinity,0.2}'::real[]); + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'most_common_elems', '{1,2,3}'::text, + 'most_common_elem_freqs', '{0.1,0.2,NaN}'::real[]); + +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'tags', + 'inherited', false::boolean, + 'elem_count_histogram', '{1,1,-Infinity,1}'::real[]); + +-- ok: a negative n_distinct is a ratio, not a count, and is still accepted +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'attname', 'id', + 'inherited', false::boolean, + 'n_distinct', '-0.5'::real); + -- ok: just the fixed values, with version, no stakinds SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', -- 2.47.3