PyUnicode_AsUTF8 returns NULL on encoding errors (e.g., lone surrogates). The return value is used without NULL checks in ~20 sites across 4 files. The most critical sites pass NULL to strcmp() — undefined behavior, crashes immediately.
DynamicScope_getitem — segfault (dynamicscope.cpp:613-640)
Every attribute lookup in enaml's declarative scope hits this code. The function calls PyUnicode_AsUTF8(key) repeatedly and passes the result to strcmp("self"), strcmp("change"), etc.
Reproducer:
from enaml.core._dynamicscope import _DynamicScope
class FakeOwner: pass
ds = _DynamicScope(FakeOwner(), {}, {}, {})
ds["\ud800"] # Segmentation fault
DynamicScope___contains__ — segfault (dynamicscope.cpp:746-762)
Same pattern in the sq_contains slot.
Reproducer:
from enaml.core._dynamicscope import _DynamicScope
class FakeOwner: pass
ds = _DynamicScope(FakeOwner(), {}, {}, {})
"\ud800" in ds # Segmentation fault
Nonlocals_repr — segfault (dynamicscope.cpp:354)
PyObject_Str(self->owner) can return a string with surrogates if __str__ does. The result is passed to PyUnicode_AsUTF8 then to PyUnicode_FromFormat with %s — UB with NULL.
Reproducer:
from enaml.core._dynamicscope import _DynamicScope
class FakeOwner:
def __str__(self): return "owner\ud800name"
ds = _DynamicScope(FakeOwner(), {}, {}, {})
repr(ds["nonlocals"]) # Segmentation fault
Also affected (SystemError, not segfault)
Font_repr (fontext.cpp:147) — PyUnicode_AsUTF8(self->family) to ostream
DFunc_repr, BoundDMethod_repr (declarative_function.cpp:179,182,343,346,349) — PyUnicode_AsUTF8 to ostream
alias_load_fail (alias.cpp:87,94) — PyUnicode_AsUTF8 to ostream
Nonlocals_getattro/setattro error messages (dynamicscope.cpp:404,419) — PyUnicode_AsUTF8 to PyErr_Format
set_dynamic_attr error message (dynamicscope.cpp:299) — same
Fix for DynamicScope — cache the result once and check:
const char* key_str = PyUnicode_AsUTF8( key );
if( !key_str )
return 0;
if( strcmp( key_str, "self" ) == 0 )
return cppy::incref( self->owner );
// ... etc
This also eliminates the redundant PyUnicode_AsUTF8 calls (performance improvement).
Found by cext-review-toolkit.
PyUnicode_AsUTF8returns NULL on encoding errors (e.g., lone surrogates). The return value is used without NULL checks in ~20 sites across 4 files. The most critical sites pass NULL tostrcmp()— undefined behavior, crashes immediately.DynamicScope_getitem — segfault (
dynamicscope.cpp:613-640)Every attribute lookup in enaml's declarative scope hits this code. The function calls
PyUnicode_AsUTF8(key)repeatedly and passes the result tostrcmp("self"),strcmp("change"), etc.Reproducer:
DynamicScope___contains__ — segfault (
dynamicscope.cpp:746-762)Same pattern in the
sq_containsslot.Reproducer:
Nonlocals_repr — segfault (
dynamicscope.cpp:354)PyObject_Str(self->owner)can return a string with surrogates if__str__does. The result is passed toPyUnicode_AsUTF8then toPyUnicode_FromFormatwith%s— UB with NULL.Reproducer:
Also affected (SystemError, not segfault)
Font_repr(fontext.cpp:147) —PyUnicode_AsUTF8(self->family)to ostreamDFunc_repr,BoundDMethod_repr(declarative_function.cpp:179,182,343,346,349) —PyUnicode_AsUTF8to ostreamalias_load_fail(alias.cpp:87,94) —PyUnicode_AsUTF8to ostreamNonlocals_getattro/setattroerror messages (dynamicscope.cpp:404,419) —PyUnicode_AsUTF8toPyErr_Formatset_dynamic_attrerror message (dynamicscope.cpp:299) — sameFix for DynamicScope — cache the result once and check:
This also eliminates the redundant
PyUnicode_AsUTF8calls (performance improvement).Found by cext-review-toolkit.