From 07cb8b5d166762149e52ef820dbbc8b3717824c9 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Tue, 14 Jul 2026 04:42:42 -0400 Subject: [PATCH] v.class: Fix out-of-bounds read when no records are selected An empty WHERE result left an empty data array, which the classification algorithms then indexed at data[0] and data[count - 1]. The values printed as class breaks were whatever the heap happened to hold, which is zeroed on Linux but not on Windows, making the test flaky there. Warn and keep the zero-initialized breaks when nothing is selected, and reject an empty array in AS_class_apply_algorithm. --- lib/arraystats/class.c | 3 ++ vector/v.class/main.c | 42 +++++++++++++++++----------- vector/v.class/tests/test_v_class.py | 6 ++-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/arraystats/class.c b/lib/arraystats/class.c index 58a4893a212..235de1fa6ff 100644 --- a/lib/arraystats/class.c +++ b/lib/arraystats/class.c @@ -27,6 +27,9 @@ double AS_class_apply_algorithm(int algo, const double data[], int nrec, { double finfo = 0.0; + if (nrec < 1) + G_fatal_error(_("Cannot classify an empty set of values")); + switch (algo) { case CLASS_INTERVAL: finfo = AS_class_interval(data, nrec, *nbreaks, classbreaks); diff --git a/vector/v.class/main.c b/vector/v.class/main.c index 0963b0394cf..324c9481456 100644 --- a/vector/v.class/main.c +++ b/vector/v.class/main.c @@ -38,9 +38,9 @@ int main(int argc, char *argv[]) int ofield; int nrec, ctype, nbclass, nbreaks, *frequencies; int ret, i; - double finfo; + double finfo = 0; double *classbreaks, min, max, *data; - struct GASTATS stats; + struct GASTATS stats = {0}; char *desc, *fs; enum OutputFormat format; G_JSON_Value *root_value = NULL, *intervals_value = NULL, @@ -174,6 +174,9 @@ int main(int argc, char *argv[]) if (nrec < 0) G_fatal_error(_("Unable to select data from table")); + if (nrec == 0) + G_warning(_("No records selected from table <%s>"), Fi->table); + db_close_database_shutdown_driver(Driver); ret = db_CatValArray_sort_by_value(&Cvarr); @@ -203,15 +206,19 @@ int main(int argc, char *argv[]) /* Get classbreaks for given algorithm and number of classbreaks. * finfo takes any info coming from the classification algorithms - * equ algorithm can alter number of class breaks */ - finfo = AS_class_apply_algorithm(AS_option_to_algorithm(algo_opt), data, - nrec, &nbreaks, classbreaks); - - if (G_strcasecmp(algo_opt->answer, "dis") == 0 && finfo < 3.84148) - G_warning( - _("The discontinuities algorithm indicates that some " - "class breaks are not statistically significant at " - "alpha=0.05. You are advised to reduce the number of classes.")); + * equ algorithm can alter number of class breaks. + * With no data there is nothing to classify and the class breaks stay + * at zero. */ + if (nrec > 0) { + finfo = AS_class_apply_algorithm(AS_option_to_algorithm(algo_opt), data, + nrec, &nbreaks, classbreaks); + + if (G_strcasecmp(algo_opt->answer, "dis") == 0 && finfo < 3.84148) + G_warning(_("The discontinuities algorithm indicates that some " + "class breaks are not statistically significant at " + "alpha=0.05. You are advised to reduce the number of " + "classes.")); + } /*output to be piped to other modules ? */ if (breaks_flag->answer) { @@ -280,12 +287,15 @@ int main(int argc, char *argv[]) for (i = 0; i < nbreaks + 1; i++) frequencies[i] = 0; - ret = - AS_class_frequencies(data, nrec, nbreaks, classbreaks, frequencies); - AS_basic_stats(data, nrec, &stats); + min = max = 0; + if (nrec > 0) { + ret = AS_class_frequencies(data, nrec, nbreaks, classbreaks, + frequencies); + AS_basic_stats(data, nrec, &stats); - min = data[0]; - max = data[nrec - 1]; + min = data[0]; + max = data[nrec - 1]; + } /* as equ algorithm can modify number of breaks we recalculate number of * classes diff --git a/vector/v.class/tests/test_v_class.py b/vector/v.class/tests/test_v_class.py index c54f7e0894c..e38d39ca51d 100644 --- a/vector/v.class/tests/test_v_class.py +++ b/vector/v.class/tests/test_v_class.py @@ -286,11 +286,11 @@ def test_v_class_where_no_matches(setup_vector_with_values): where="value > 100", # No points satisfy this algorithm="int", nbclasses=5, - flags="g", + flags="b", env=session.env, ) - breaks = [float(b) for b in output.strip().split(",")] if output.strip() else [] - assert len(breaks) == 4 # For 5 classes → 4 breaks + breaks = [float(b) for b in output.strip().split(",")] + assert len(breaks) == 4 # For 5 classes, there are 4 breaks assert all(b == 0.0 for b in breaks)