Skip to content

Segfaults from PyUnicode_AsUTF8 NULL passed to strcmp in DynamicScope hot path + repr functions #599

Description

@devdanzin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions