From 87444c2fd07993c43ee9b5cedd94f3c304a05029 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 11 Jan 2026 20:26:04 -0500 Subject: [PATCH] HeightMap: add GRID_MAX limit and input validation Fixes potential integer overflow and invalid input issues: - Add GRID_MAX constant (8192) to Common.h for global use - Validate HeightMap dimensions against GRID_MAX to prevent integer overflow in w*h calculations (65536*65536 = 0) - Add min > max validation for clamp() and normalize() - Add unit tests for all new validation cases Co-Authored-By: Claude Opus 4.5 --- src/Common.h | 4 ++ src/PyHeightMap.cpp | 17 ++++++++ tests/unit/test_heightmap_basic.py | 69 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/src/Common.h b/src/Common.h index 2bdc3ba..f3c3b34 100644 --- a/src/Common.h +++ b/src/Common.h @@ -2,6 +2,10 @@ #include #include +// Maximum dimension for grids, layers, and heightmaps (8192x8192 = 256MB of float data) +// Prevents integer overflow in size calculations and limits memory allocation +constexpr int GRID_MAX = 8192; + #include #include #include diff --git a/src/PyHeightMap.cpp b/src/PyHeightMap.cpp index d38e299..e263d14 100644 --- a/src/PyHeightMap.cpp +++ b/src/PyHeightMap.cpp @@ -102,6 +102,13 @@ int PyHeightMap::init(PyHeightMapObject* self, PyObject* args, PyObject* kwds) return -1; } + if (width > GRID_MAX || height > GRID_MAX) { + PyErr_Format(PyExc_ValueError, + "HeightMap dimensions cannot exceed %d (got %dx%d)", + GRID_MAX, width, height); + return -1; + } + // Clean up any existing heightmap if (self->heightmap) { TCOD_heightmap_delete(self->heightmap); @@ -253,6 +260,11 @@ PyObject* PyHeightMap::clamp(PyHeightMapObject* self, PyObject* args, PyObject* return nullptr; } + if (min_val > max_val) { + PyErr_SetString(PyExc_ValueError, "min must be less than or equal to max"); + return nullptr; + } + TCOD_heightmap_clamp(self->heightmap, min_val, max_val); // Return self for chaining @@ -277,6 +289,11 @@ PyObject* PyHeightMap::normalize(PyHeightMapObject* self, PyObject* args, PyObje return nullptr; } + if (min_val > max_val) { + PyErr_SetString(PyExc_ValueError, "min must be less than or equal to max"); + return nullptr; + } + TCOD_heightmap_normalize(self->heightmap, min_val, max_val); // Return self for chaining diff --git a/tests/unit/test_heightmap_basic.py b/tests/unit/test_heightmap_basic.py index 9dd1279..2191508 100644 --- a/tests/unit/test_heightmap_basic.py +++ b/tests/unit/test_heightmap_basic.py @@ -167,6 +167,71 @@ def test_invalid_size_type(): print("PASS: test_invalid_size_type") +def test_size_exceeds_grid_max(): + """Size exceeding GRID_MAX (8192) raises ValueError""" + # Test width exceeds limit + try: + mcrfpy.HeightMap((10000, 100)) + print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for width=10000") + sys.exit(1) + except ValueError as e: + assert "8192" in str(e) or "cannot exceed" in str(e).lower() + + # Test height exceeds limit + try: + mcrfpy.HeightMap((100, 10000)) + print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for height=10000") + sys.exit(1) + except ValueError as e: + assert "8192" in str(e) or "cannot exceed" in str(e).lower() + + # Test both exceed limit (would cause integer overflow without validation) + try: + mcrfpy.HeightMap((65536, 65536)) + print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for 65536x65536") + sys.exit(1) + except ValueError: + pass + + print("PASS: test_size_exceeds_grid_max") + + +def test_clamp_min_greater_than_max(): + """clamp() with min > max raises ValueError""" + hmap = mcrfpy.HeightMap((10, 10), fill=0.5) + try: + hmap.clamp(min=1.0, max=0.0) + print("FAIL: test_clamp_min_greater_than_max - should have raised ValueError") + sys.exit(1) + except ValueError as e: + assert "min" in str(e).lower() and "max" in str(e).lower() + print("PASS: test_clamp_min_greater_than_max") + + +def test_normalize_min_greater_than_max(): + """normalize() with min > max raises ValueError""" + hmap = mcrfpy.HeightMap((10, 10), fill=0.5) + try: + hmap.normalize(min=1.0, max=0.0) + print("FAIL: test_normalize_min_greater_than_max - should have raised ValueError") + sys.exit(1) + except ValueError as e: + assert "min" in str(e).lower() and "max" in str(e).lower() + print("PASS: test_normalize_min_greater_than_max") + + +def test_max_valid_size(): + """Size at GRID_MAX boundary works""" + # Test at the exact limit - this should work + hmap = mcrfpy.HeightMap((8192, 1)) + assert hmap.size == (8192, 1) + + hmap2 = mcrfpy.HeightMap((1, 8192)) + assert hmap2.size == (1, 8192) + + print("PASS: test_max_valid_size") + + def run_all_tests(): """Run all tests""" print("Running HeightMap basic tests...") @@ -189,6 +254,10 @@ def run_all_tests(): test_repr() test_invalid_size() test_invalid_size_type() + test_size_exceeds_grid_max() + test_clamp_min_greater_than_max() + test_normalize_min_greater_than_max() + test_max_valid_size() print() print("All HeightMap basic tests PASSED!")