Check Python C-API returns in terrain_enum builders; closes #322

WangSet.terrain_enum (and the parallel AutoRuleSet.terrain_enum) built a
Python IntEnum from parsed terrain/color names but ignored the return codes
of PyLong_FromLong, PyUnicode_FromString, and PyDict_SetItemString. A name
carrying invalid UTF-8 -- trivially reachable from an untrusted .tsx/.ldtk
import -- makes those calls fail and leave an exception pending. The code
then called PyObject_Call with that error already set, tripping CPython's
_PyErr_Occurred assertion and aborting (libFuzzer: deadly signal).

Both functions now check every C-API return, clean up their references, and
return NULL so the real exception (e.g. UnicodeDecodeError) propagates.

Verified by A/B replay of the #312 fuzz_import_parsers crash corpus under
clang-18 ASan/UBSan: the saved crash input aborts pre-fix inside
PyWangSet::terrain_enum -> PyObject_Call and runs clean post-fix. New
regression test crafts a .tsx with invalid-UTF-8 wangset/wangcolor names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
This commit is contained in:
John McCardle 2026-06-26 23:52:38 -04:00
commit a2c76c3643
3 changed files with 138 additions and 4 deletions

View file

@ -129,21 +129,44 @@ PyObject* PyWangSet::terrain_enum(PyWangSetObject* self, PyObject*) {
// NONE = 0 (unset terrain)
PyObject* zero = PyLong_FromLong(0);
PyDict_SetItemString(members, "NONE", zero);
if (!zero || PyDict_SetItemString(members, "NONE", zero) < 0) {
Py_XDECREF(zero);
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
Py_DECREF(zero);
for (const auto& wc : ws.colors) {
std::string key = toUpperSnakeCase(wc.name);
PyObject* val = PyLong_FromLong(wc.index);
PyDict_SetItemString(members, key.c_str(), val);
// A wang color name carrying invalid UTF-8 (untrusted import data) makes
// PyDict_SetItemString fail and leaves an exception pending; bail now
// rather than calling more Python API with that error set, which trips a
// _PyErr_Occurred assertion / abort. #322
if (!val || PyDict_SetItemString(members, key.c_str(), val) < 0) {
Py_XDECREF(val);
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
Py_DECREF(val);
}
// Create enum class: IntEnum(ws.name, members)
PyObject* name = PyUnicode_FromString(ws.name.c_str());
if (!name) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
PyObject* args = PyTuple_Pack(2, name, members);
Py_DECREF(name);
Py_DECREF(members);
if (!args) {
Py_DECREF(int_enum);
return NULL;
}
PyObject* enum_class = PyObject_Call(int_enum, args, NULL);
Py_DECREF(args);