Skip to content

Fix MethodBinding/OverloadMapper memory leak (#691)#2719

Merged
filmor merged 3 commits into
pythonnet:masterfrom
greateggsgreg:memleak-691
May 12, 2026
Merged

Fix MethodBinding/OverloadMapper memory leak (#691)#2719
filmor merged 3 commits into
pythonnet:masterfrom
greateggsgreg:memleak-691

Conversation

@greateggsgreg
Copy link
Copy Markdown
Contributor

@greateggsgreg greateggsgreg commented May 9, 2026

Fixes #691.

Cause

MethodBinding and OverloadMapper hold a PyObject target but didn't release it on tp_clear, so the underlying CLR instance waited on the .NET finalizer chain to drop the refcount. They also shared the same C# PyObject instance across mp_subscript / Overloads paths, so disposing one wrapper corrupted the others.

Fix

  • ExtensionType: add virtual OnClear() hook called from tp_clear.
  • MethodBinding / OverloadMapper: override OnClear to dispose target. (targetType left alone — disposing it broke unrelated subclass tests.)
  • Each sharing site now passes new PyObject(self.target.Reference) so each wrapper owns its own INCREF'd reference.

Tests

The three existing *_does_not_leak_memory tests cover the three sharing sites but their 0.9 MB/iter threshold was too loose — master was leaking ~600 KB/iter and still passing. Tightened to 0.1 MB/iter (104 KB).

Verification (Python 3.14 GIL, linux-aarch64)

.NET 8 .NET 10
master FAIL 765 KB/iter FAIL 572-647 KB/iter
this PR PASS -0.6 KB/iter PASS -0.5 KB/iter

MethodBinding and OverloadMapper held PyObject `target` references that
were not disposed during tp_clear, leaving Python-side refcount drops to
wait on the multi-hop .NET finalizer chain. They also shared the same
C# PyObject instance across mp_subscript/Overloads paths, so freeing one
could free the underlying Python object out from under the others.

- ExtensionType: add virtual OnClear() hook called from tp_clear before
  the GCHandle is released, letting subclasses eagerly drop owned
  Python references.
- MethodBinding/OverloadMapper: override OnClear to dispose `target`.
  (`targetType` is intentionally not disposed since Python types are
  long-lived and tracked by other caches.)
- Take an independent INCREF'd PyObject copy at every site that hands a
  shared target into a new MethodBinding or OverloadMapper, so each
  wrapper owns its own reference.

Result: the three _does_not_leak_memory tests drop from ~485 MB delta
to ~10 KB delta on Python 3.14.
The previous 90% threshold (0.9 MB/iter against a 1 MB allocation)
documented the issue but did not reproduce it: master leaks
~600-765 KB/iter, which the 0.9 MB threshold accepts as passing.

Drop the threshold to 10% (104 KB/iter). On the 2026-05-09 verification
run with Python 3.14 GIL on linux-aarch64:

  Without fix (master):   ~572-765 KB/iter (FAIL)
  With fix (this branch): ~-500 B/iter     (PASS)

Margin is roughly 6x in either direction across .NET 8 and .NET 10, so
the threshold cleanly separates buggy from fixed states without being
sensitive to GC noise.
@lostmsu
Copy link
Copy Markdown
Member

lostmsu commented May 11, 2026

If the objects are not shared anymore and are always owned by MethodBinding the NewReference is a better type to hold it than PyObject.

Comment thread src/runtime/Types/ExtensionType.cs
@greateggsgreg
Copy link
Copy Markdown
Contributor Author

greateggsgreg commented May 12, 2026

NewReference is declared as a ref struct, so it can't be a class field. The closest owned type is PyObject. Unless I'm missing something? We'd also have to swap out a few of the callsites in MethodBinding that expect a PyObject

Ownership is still explicit: OnClear disposes it, the field has no external readers, and every sharing site INCREFs into its own Python object.

- Handle the `PyType` reference in `OverloadMapper` and `MethodBinding` in the
  same way as the object reference
- Unconditionally store the `PyType` of the object
- Introduce `NewReference` helper function for the object and type
  passing
- Fix the remaining missing reference count bump for the type
  (`MethodObject`)
- As the count is now correct, `Dispose` the type as well
@filmor
Copy link
Copy Markdown
Member

filmor commented May 12, 2026

@greateggsgreg I took the liberty of pushing my notes as a commit :)

  • There was one missing location for the ownership transfer of the passed types
  • Now that this is solved, we can Dispose the type object as well
  • I introduced helper functions for the refcount bump

If these are fine with you, I'd go ahead and merge this once the CI is through.

@filmor filmor merged commit ca323cc into pythonnet:master May 12, 2026
50 of 56 checks passed
@greateggsgreg
Copy link
Copy Markdown
Contributor Author

Awesome, looks good. Thank you!

@greateggsgreg greateggsgreg deleted the memleak-691 branch May 13, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory Leak on MethodBinding for generic method

3 participants