You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Binder: closing a PyObject argument inside an upcall crashes later (SIGSEGV in Py_XDECREF on the Cleaner thread) #98
A KSP-bound Kotlin function receives a Python object as PyObject? and calls close() on it inside the call. The JVM later dies with SIGSEGV in Py_XDECREF on the Cleaner-0 thread, during PyObjectKt.topLevelDecRefAction → CleanupAction.run. In other words, one reference is released twice.
funtpTouch(token:PyObject?): Long { token?.close(); return1L }
UpcallTrampoline.kt:71 documents arguments as borrowed, with the wrapper built borrowed = true so that it takes its own reference. On that basis close() should hand back exactly the reference the wrapper took.
With close():Gradle Test Executor exits 134. hs_err: SIGSEGV at libpython3.14.dylib Py_XDECREF; the Java frames are topLevelDecRefAction ← PyObject$1.invoke ← CleanupAction.run ← PhantomCleanableRef.performCleanup (Cleaner-0).
Without close(): no crash. After 10,000 calls the token's sys.getrefcount has gone from 10 to 17,350 until the Kotlin collector runs. Release is deferred, as designed.
Where to look
PyAutoCloseable.close() → PlatformCleaner.close(): Java's Cleanable.clean() is idempotent, so close() plus a later Cleaner run should not release twice by itself. The second release must come from somewhere else. Candidates:
the trampoline or argument conversion also dropping the wrapper's reference;
a wrapper built borrowed = false on this path;
two wrappers over one pointer.
AGENTS §16's reference-convention warning describes exactly this failure.
Done when
A ksp-fixtures test that closes a PyObject argument inside an upcall, 10,000 times, runs clean and leaves the refcount unchanged once the Kotlin collector has run. Nothing is released twice.
Update: close() is not needed to trigger it. The #45 test was changed to never close the argument and to force collection instead: System.gc() and System.runFinalization() in a loop after 10,000 upcalls, until the refcounts settle. The test JVM then died the same way: SIGSEGV in Py_XDECREF on Cleaner-0, in PyObjectKt.topLevelDecRefAction ← CleanupAction.run (ksp-fixtures/app/hs_err_pid84367.log in the TypedPython #45 worktree).
Without forced GC, the token's refcount reads 10 → 17,350 after the loop, so the wrappers did take references. When the Cleaner later releases them, at least one reference is released one time too many. So the double release is on the ordinary Cleaner path for wrappers created during upcalls, not specific to close(). Any long-running app that passes Python objects to Kotlin would eventually hit it once GC runs.
Which wrapper is released twice is not yet established: the token argument, the TpSample proxy objects built by the constructor, or the callables. A bisect would run the loop with only one of the three kinds of calls at a time.
Activity
b-re-w commentedon Oct 3, 2026
Update:
close()is not needed to trigger it. The #45 test was changed to never close the argument and to force collection instead:System.gc()andSystem.runFinalization()in a loop after 10,000 upcalls, until the refcounts settle. The test JVM then died the same way: SIGSEGV inPy_XDECREFonCleaner-0, inPyObjectKt.topLevelDecRefAction←CleanupAction.run(ksp-fixtures/app/hs_err_pid84367.login the TypedPython #45 worktree).Without forced GC, the token's refcount reads 10 → 17,350 after the loop, so the wrappers did take references. When the Cleaner later releases them, at least one reference is released one time too many. So the double release is on the ordinary Cleaner path for wrappers created during upcalls, not specific to
close(). Any long-running app that passes Python objects to Kotlin would eventually hit it once GC runs.Which wrapper is released twice is not yet established: the token argument, the
TpSampleproxy objects built by the constructor, or the callables. A bisect would run the loop with only one of the three kinds of calls at a time.b-re-w commentedon Oct 3, 2026
Landed in develop via #118.
Merge pull request #119 from thisisthepy/feat/tp-leak-test