Skip to content

Commit 42c0f77

Browse files
committed
Release callback dicts by registry membership, never by type-guessing
9.5.39's finalization guard missed the real bug: the crash fires during a plain lv.deinit() call with the interpreter fully alive (gdb: the frames above py_lvgl_deinit are PyObject_Vectorcall, not Py_FinalizeEx - atexit hooks run before CPython sets the finalizing flag anyway). py_lv_delete_cb sweeps every event descriptor on a dying object, including registrations LVGL itself made, and probing a foreign C pointer with PyDict_Check reads ob_type from non-PyObject memory - the observed segfault. A pointer is now releasable iff this runtime recorded it when it created the per-registration dict (both PyDict_New sites register; release is registry-remove + DECREF; lookups replace every type probe). Foreign user_data is never touched. The finalizing skip stays as a belt. Proof: the analog_clock reproducer (TestPyPI pydevices-desktop + this build, SDL_VIDEODRIVER=dummy) segfaulted rc=139 on 9.5.38/9.5.39 and now exits 0, three runs under MALLOC_PERTURB_; suite 6/6 green.
1 parent c28224a commit 42c0f77

1 file changed

Lines changed: 47 additions & 17 deletions

File tree

src/lvpy_runtime.c

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -712,31 +712,59 @@ PyObject *get_callback_dict_from_user_data(void *user_data)
712712
#define LVPY_IS_FINALIZING() _Py_IsFinalizing()
713713
#endif
714714

715+
/* Registry of the per-registration callback dicts THIS runtime created.
716+
*
717+
* py_lv_delete_cb sweeps every event descriptor on a dying object --
718+
* including registrations made by LVGL internals or other C code, whose
719+
* user_data is an arbitrary C pointer. Probing such a pointer with
720+
* PyDict_Check() dereferences ob_type on non-PyObject memory and can
721+
* segfault (observed under lv_deinit(): a foreign dsc's user_data pointed
722+
* into an LVGL heap block). Never type-guess: a pointer is releasable iff
723+
* we recorded it at creation. All access happens with the GIL held
724+
* (mp_lv_callback runs from Python; py_lv_delete_cb takes the GIL). */
725+
static void **lvpy_dict_registry = NULL;
726+
static size_t lvpy_dict_registry_len = 0;
727+
static size_t lvpy_dict_registry_cap = 0;
728+
729+
void lvpy_register_callback_dict(void *user_data)
730+
{
731+
if (!user_data) return;
732+
if (lvpy_dict_registry_len == lvpy_dict_registry_cap) {
733+
size_t cap = lvpy_dict_registry_cap ? lvpy_dict_registry_cap * 2 : 32;
734+
void **grown = (void **)realloc(lvpy_dict_registry, cap * sizeof(void *));
735+
if (!grown) return; /* untracked dict leaks at release time; safe */
736+
lvpy_dict_registry = grown;
737+
lvpy_dict_registry_cap = cap;
738+
}
739+
lvpy_dict_registry[lvpy_dict_registry_len++] = user_data;
740+
}
741+
742+
static int lvpy_registry_remove(void *user_data)
743+
{
744+
for (size_t i = 0; i < lvpy_dict_registry_len; i++) {
745+
if (lvpy_dict_registry[i] == user_data) {
746+
lvpy_dict_registry[i] = lvpy_dict_registry[--lvpy_dict_registry_len];
747+
return 1;
748+
}
749+
}
750+
return 0;
751+
}
752+
715753
int lvpy_is_per_registration_callback_dict(void *user_data)
716754
{
717755
if (!user_data) return 0;
718-
/* During Py_FinalizeEx(), lv_deinit()'s object-tree teardown fires
719-
* LV_EVENT_DELETE per object, which reaches here through
720-
* lvpy_release_callback_user_data(). At that point PyObject internals
721-
* (the type object graph PyDict_Check/PyObject_TypeCheck walk) may
722-
* already be torn down, so probing them is unsafe. Treat user_data as
723-
* opaque once finalization has started. */
724-
if (LVPY_IS_FINALIZING()) return 0;
725-
PyObject *obj = (PyObject *)user_data;
726-
if (!PyDict_Check(obj)) return 0;
727-
PyTypeObject *base = py_get_base_obj_type();
728-
if (base && PyObject_TypeCheck(obj, base)) return 0;
729-
return 1;
756+
for (size_t i = 0; i < lvpy_dict_registry_len; i++) {
757+
if (lvpy_dict_registry[i] == user_data) return 1;
758+
}
759+
return 0;
730760
}
731761

732762
void lvpy_release_callback_user_data(void *user_data)
733763
{
734-
/* Same finalization hazard as above: Py_DECREF touches the object's
735-
* type/refcount machinery, which is unsafe once Py_FinalizeEx() has
736-
* started tearing down the interpreter. Skip the release and leak —
737-
* the process is exiting, so this memory is reclaimed by the OS. */
764+
/* Belt on top of the registry: once Py_FinalizeEx() is tearing the
765+
* interpreter down, skip the DECREF and leak; the OS reclaims it. */
738766
if (LVPY_IS_FINALIZING()) return;
739-
if (lvpy_is_per_registration_callback_dict(user_data)) {
767+
if (user_data && lvpy_registry_remove(user_data)) {
740768
Py_DECREF((PyObject *)user_data);
741769
}
742770
}
@@ -843,6 +871,7 @@ void *mp_lv_callback(PyObject *py_callback, void *lv_callback, const char *callb
843871
*user_data_ptr = PyDict_New();
844872
if (!*user_data_ptr) return NULL;
845873
Py_INCREF((PyObject *)*user_data_ptr);
874+
lvpy_register_callback_dict(*user_data_ptr);
846875
} else if (PyDict_Check((PyObject *)*user_data_ptr)) {
847876
Py_INCREF((PyObject *)*user_data_ptr);
848877
}
@@ -851,6 +880,7 @@ void *mp_lv_callback(PyObject *py_callback, void *lv_callback, const char *callb
851880
user_data = get_user_data(containing_struct);
852881
if (!user_data) {
853882
user_data = PyDict_New();
883+
lvpy_register_callback_dict(user_data);
854884
set_user_data(containing_struct, user_data);
855885
}
856886
}

0 commit comments

Comments
 (0)