Bounds-check DijkstraMap coordinates at the Python boundary
DijkstraMap.distance, .path_from, and .step_from forwarded their coordinate arguments straight into TCOD's dijkstra routines, which assert and abort() the entire process on out-of-bounds input. No recoverable Python exception; the whole interpreter dies. Validate at the binding layer. Out-of-range coordinates raise IndexError with a message identifying the map dimensions. Fuzz corpus crash tests/fuzz/crashes/pathfinding_behavior-crash-b7ea442fd31774b9b16c8ae99c728f609c8c25d8 now runs cleanly. closes #311 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19b43ce5fa
commit
417fc43325
2 changed files with 86 additions and 0 deletions
|
|
@ -289,6 +289,21 @@ PyObject* UIGridPathfinding::DijkstraMap_repr(PyDijkstraMapObject* self) {
|
|||
return PyUnicode_FromFormat("<DijkstraMap root=(%d,%d)>", root.x, root.y);
|
||||
}
|
||||
|
||||
// #311: Reject out-of-bounds coordinates at the Python boundary. TCOD's
|
||||
// dijkstra routines assert and abort() the process for invalid coords, so
|
||||
// catching them here turns a fatal crash into a recoverable IndexError.
|
||||
static bool dijkstra_bounds_check(DijkstraMap* dmap, int x, int y) {
|
||||
int w = dmap->getWidth();
|
||||
int h = dmap->getHeight();
|
||||
if (x < 0 || y < 0 || x >= w || y >= h) {
|
||||
PyErr_Format(PyExc_IndexError,
|
||||
"coordinate (%d, %d) out of bounds for DijkstraMap of size %dx%d",
|
||||
x, y, w, h);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"pos", NULL};
|
||||
PyObject* pos_obj = NULL;
|
||||
|
|
@ -307,6 +322,8 @@ PyObject* UIGridPathfinding::DijkstraMap_distance(PyDijkstraMapObject* self, PyO
|
|||
return NULL;
|
||||
}
|
||||
|
||||
if (!dijkstra_bounds_check(self->data.get(), x, y)) return NULL;
|
||||
|
||||
float dist = self->data->getDistance(x, y);
|
||||
if (dist < 0) {
|
||||
Py_RETURN_NONE; // Unreachable
|
||||
|
|
@ -333,6 +350,8 @@ PyObject* UIGridPathfinding::DijkstraMap_path_from(PyDijkstraMapObject* self, Py
|
|||
return NULL;
|
||||
}
|
||||
|
||||
if (!dijkstra_bounds_check(self->data.get(), x, y)) return NULL;
|
||||
|
||||
std::vector<sf::Vector2i> path = self->data->getPathFrom(x, y);
|
||||
|
||||
// Create an AStarPath object to return
|
||||
|
|
@ -366,6 +385,8 @@ PyObject* UIGridPathfinding::DijkstraMap_step_from(PyDijkstraMapObject* self, Py
|
|||
return NULL;
|
||||
}
|
||||
|
||||
if (!dijkstra_bounds_check(self->data.get(), x, y)) return NULL;
|
||||
|
||||
bool valid = false;
|
||||
sf::Vector2i step = self->data->stepFrom(x, y, &valid);
|
||||
|
||||
|
|
|
|||
65
tests/regression/issue_311_dijkstra_bounds_test.py
Normal file
65
tests/regression/issue_311_dijkstra_bounds_test.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Regression test for issue #311.
|
||||
|
||||
DijkstraMap.distance, .path_from, and .step_from previously forwarded
|
||||
out-of-bounds coordinates directly to TCOD's dijkstra routines, which
|
||||
call abort() on assertion failure. The fuzz target fuzz_pathfinding_behavior
|
||||
surfaced a specific crash input.
|
||||
|
||||
Fixed by bounds-checking at the Python binding layer: out-of-range coords
|
||||
now raise IndexError with a message including the map dimensions.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def _make_dmap(w, h):
|
||||
grid = mcrfpy.Grid(grid_size=(w, h))
|
||||
return grid.get_dijkstra_map((w // 2, h // 2))
|
||||
|
||||
|
||||
def _expect_index_error(fn, label):
|
||||
try:
|
||||
fn()
|
||||
except IndexError:
|
||||
return
|
||||
except Exception as exc:
|
||||
print(f"FAIL: {label} raised {type(exc).__name__}: {exc} (expected IndexError)")
|
||||
sys.exit(1)
|
||||
print(f"FAIL: {label} did not raise")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
dmap = _make_dmap(10, 8)
|
||||
|
||||
# Valid in-bounds coordinates should work
|
||||
_ = dmap.distance((0, 0))
|
||||
_ = dmap.distance((9, 7)) # corner
|
||||
_ = dmap.distance((5, 4)) # middle
|
||||
|
||||
# Each out-of-range call must raise IndexError
|
||||
_expect_index_error(lambda: dmap.distance((-1, 0)), "distance neg x")
|
||||
_expect_index_error(lambda: dmap.distance((0, -1)), "distance neg y")
|
||||
_expect_index_error(lambda: dmap.distance((10, 0)), "distance x>=width")
|
||||
_expect_index_error(lambda: dmap.distance((0, 8)), "distance y>=height")
|
||||
_expect_index_error(lambda: dmap.distance((9999, 9999)), "distance huge")
|
||||
|
||||
_expect_index_error(lambda: dmap.path_from((-5, -5)), "path_from negative")
|
||||
_expect_index_error(lambda: dmap.path_from((100, 100)), "path_from too large")
|
||||
|
||||
_expect_index_error(lambda: dmap.step_from((-1, -1)), "step_from negative")
|
||||
_expect_index_error(lambda: dmap.step_from((10, 8)), "step_from on boundary")
|
||||
|
||||
# After a bounds violation, the dmap must still be usable (no abort, no
|
||||
# torn-down TCOD state)
|
||||
_ = dmap.distance((1, 1))
|
||||
_ = dmap.path_from((2, 2))
|
||||
_ = dmap.step_from((3, 3))
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue