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 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-11 20:26:04 -05:00
commit 87444c2fd0
3 changed files with 90 additions and 0 deletions

View file

@ -2,6 +2,10 @@
#include <SFML/Graphics.hpp> #include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp> #include <SFML/Audio.hpp>
// 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 <vector> #include <vector>
#include <iostream> #include <iostream>
#include <memory> #include <memory>

View file

@ -102,6 +102,13 @@ int PyHeightMap::init(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
return -1; 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 // Clean up any existing heightmap
if (self->heightmap) { if (self->heightmap) {
TCOD_heightmap_delete(self->heightmap); TCOD_heightmap_delete(self->heightmap);
@ -253,6 +260,11 @@ PyObject* PyHeightMap::clamp(PyHeightMapObject* self, PyObject* args, PyObject*
return nullptr; 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); TCOD_heightmap_clamp(self->heightmap, min_val, max_val);
// Return self for chaining // Return self for chaining
@ -277,6 +289,11 @@ PyObject* PyHeightMap::normalize(PyHeightMapObject* self, PyObject* args, PyObje
return nullptr; 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); TCOD_heightmap_normalize(self->heightmap, min_val, max_val);
// Return self for chaining // Return self for chaining

View file

@ -167,6 +167,71 @@ def test_invalid_size_type():
print("PASS: 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(): def run_all_tests():
"""Run all tests""" """Run all tests"""
print("Running HeightMap basic tests...") print("Running HeightMap basic tests...")
@ -189,6 +254,10 @@ def run_all_tests():
test_repr() test_repr()
test_invalid_size() test_invalid_size()
test_invalid_size_type() 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()
print("All HeightMap basic tests PASSED!") print("All HeightMap basic tests PASSED!")