Route Grid.compute_fov algorithm through PyFOV::from_arg

The compute_fov binding parsed its `algorithm` argument as a raw int and
cast it directly to TCOD_fov_algorithm_t. Out-of-range values (e.g. -49
reinterpreted as 4294967247) triggered UBSan's "invalid enum load" deep
in GridData::computeFOV.

PyFOV::from_arg already does the right validation for every other
algorithm entry point: accepts the FOV IntEnum, ints in
[0, NB_FOV_ALGORITHMS), or None (default BASIC); raises ValueError
otherwise. Route the binding through it.

Fuzz corpus crash
tests/fuzz/crashes/fov-crash-d5da064d802ae2b5691c520907cd692d04de8bb2
now runs cleanly.

closes #310

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-04-16 23:32:09 -04:00
commit 19b43ce5fa
2 changed files with 82 additions and 4 deletions

View file

@ -14,6 +14,7 @@
#include "EntityBehavior.h"
#include "PyTrigger.h"
#include "UIBase.h"
#include "PyFOV.h"
// =========================================================================
// Cell access: py_at, subscript, mpmethods
@ -97,10 +98,10 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
PyObject* pos_obj = NULL;
int radius = 0;
int light_walls = 1;
int algorithm = FOV_BASIC;
PyObject* algorithm_obj = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipi", const_cast<char**>(kwlist),
&pos_obj, &radius, &light_walls, &algorithm)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipO", const_cast<char**>(kwlist),
&pos_obj, &radius, &light_walls, &algorithm_obj)) {
return NULL;
}
@ -109,7 +110,17 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
return NULL;
}
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
// #310: validate algorithm via PyFOV::from_arg so out-of-range ints become a
// ValueError at the Python boundary instead of a UBSan-flagged invalid enum
// load deep in GridData::computeFOV.
TCOD_fov_algorithm_t algorithm = FOV_BASIC;
if (algorithm_obj != NULL) {
if (!PyFOV::from_arg(algorithm_obj, &algorithm)) {
return NULL;
}
}
self->data->computeFOV(x, y, radius, light_walls, algorithm);
Py_RETURN_NONE;
}

View file

@ -0,0 +1,67 @@
"""Regression test for issue #310.
Grid.compute_fov's algorithm argument was passed directly as an int into
GridData::computeFOV, where it was cast to TCOD_fov_algorithm_t. Values
outside the enum range caused UBSan to report an invalid enum load. Fuzz
target fuzz_fov surfaced this with algorithm=-49.
The fix routes the argument through PyFOV::from_arg, which accepts FOV
enum members, ints in [0, NB_FOV_ALGORITHMS), or None (defaults to BASIC),
and raises ValueError for out-of-range ints.
"""
import mcrfpy
import sys
def main():
grid = mcrfpy.Grid(grid_size=(10, 10))
# Valid paths: enum, int, None, omitted
grid.compute_fov((5, 5), radius=3, algorithm=mcrfpy.FOV.BASIC)
grid.compute_fov((5, 5), radius=3, algorithm=0) # FOV_BASIC
grid.compute_fov((5, 5), radius=3, algorithm=None) # default
grid.compute_fov((5, 5), radius=3) # default, no arg
# Out-of-range negative int (this was the UBSan trigger)
try:
grid.compute_fov((5, 5), radius=3, algorithm=-49)
except ValueError:
pass
else:
print("FAIL: algorithm=-49 should raise ValueError")
sys.exit(1)
# Out-of-range positive int (above NB_FOV_ALGORITHMS sentinel)
try:
grid.compute_fov((5, 5), radius=3, algorithm=9999)
except ValueError:
pass
else:
print("FAIL: algorithm=9999 should raise ValueError")
sys.exit(1)
# Wrong type still rejected
try:
grid.compute_fov((5, 5), radius=3, algorithm="basic")
except (TypeError, ValueError):
pass
else:
print("FAIL: algorithm='basic' should raise TypeError")
sys.exit(1)
# Boundary int -1 should fail (negative)
try:
grid.compute_fov((5, 5), radius=3, algorithm=-1)
except ValueError:
pass
else:
print("FAIL: algorithm=-1 should raise ValueError")
sys.exit(1)
print("PASS")
sys.exit(0)
if __name__ == "__main__":
main()