Bug
The global msg_reflash_callback stores the borrowed argument without Py_INCREF. Once the caller releases the callback, a later firmware-progress event accesses freed Python memory.
Reviewed commit: 45167e09de6b886cd451a4245f842c8221349711 (current master on 2026-09-15).
Source: src/methods.cpp:2142
Reproduction
This reproducer uses a hardware-free DLL. Save this as mock.c:
#define API __declspec(dllexport)
typedef unsigned long long size_t;
API void icsneoSetReflashCallback(void (*cb)(const unsigned short*, unsigned long)) {}
Build with LLVM on Windows x64:
clang-cl /nologo /c /GS- /Zl mock.c /Fomock.obj
lld-link /dll /noentry /nodefaultlib /out:mock.dll mock.obj
Then run:
import ics
import ctypes
from pathlib import Path
ics.override_library_name(str(Path('mock.dll').resolve()))
d = ics.PyNeoDeviceEx()
d._auto_handle_close = False
import weakref, gc
class Handler:
def __call__(self, message, progress): pass
handler = Handler()
ref = weakref.ref(handler)
ics.set_reflash_callback(handler)
del handler
gc.collect()
print(ref() is None)
ics.set_reflash_callback(None)
Observed / expected
True: the callback has been collected while still registered. Expected it to remain alive until replacement/unregistration. The reproduction intentionally does not invoke the dangling callback. Closed #52 fixed GIL acquisition, not ownership of the callback object.
Suggested fix and regression coverage
Own a strong reference while registered, release the previous reference on replacement/unregistration, and validate callability. Handle callback exceptions and DECREF callback return values. Add weak-reference lifetime tests and a mock progress event after the caller drops its reference.
Validation environment
Windows x64, CPython 3.14.5, extension rebuilt from the commit above. Hardware-dependent entry points below were exercised with a mock DLL, not a connected device. The existing 30 tests pass despite these findings.
Bug
The global
msg_reflash_callbackstores the borrowed argument without Py_INCREF. Once the caller releases the callback, a later firmware-progress event accesses freed Python memory.Reviewed commit:
45167e09de6b886cd451a4245f842c8221349711(current master on 2026-09-15).Source: src/methods.cpp:2142
Reproduction
This reproducer uses a hardware-free DLL. Save this as
mock.c:Build with LLVM on Windows x64:
Then run:
Observed / expected
True: the callback has been collected while still registered. Expected it to remain alive until replacement/unregistration. The reproduction intentionally does not invoke the dangling callback. Closed #52 fixed GIL acquisition, not ownership of the callback object.Suggested fix and regression coverage
Own a strong reference while registered, release the previous reference on replacement/unregistration, and validate callability. Handle callback exceptions and DECREF callback return values. Add weak-reference lifetime tests and a mock progress event after the caller drops its reference.
Validation environment
Windows x64, CPython 3.14.5, extension rebuilt from the commit above. Hardware-dependent entry points below were exercised with a mock DLL, not a connected device. The existing 30 tests pass despite these findings.