Skip to content

[P1] set_reflash_callback keeps a borrowed reference that can be collected before use #241

Description

@drebbe-intrepid

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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