Skip to content

Commit d55dcaa

Browse files
jhonabreulclaude
andcommitted
Fix interpreter heap corruption across Initialize/Shutdown cycles
Several caches and the sys run counter survived PythonEngine.Shutdown and dangled into the next session, corrupting the interpreter heap on re-initialization: - Converter: add Reset() to dispose cached enum wrappers on shutdown. - Runtime: only reuse the previous sys run counter when restoring stashed AppDomain state (clr_data present); otherwise start a fresh run so leaked objects from a dead session are skipped on finalization. Call Converter.Reset() during shutdown. - LookUpObject: use indexer assignment instead of Add so re-reflecting a type in a later cycle does not throw a duplicate-key exception from the native tp_getattro callback. - TestPyObject: ignore the obsolete GetAttrDefault_IgnoresAttributeErrorOnly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6ced74e commit d55dcaa

4 files changed

Lines changed: 40 additions & 3 deletions

File tree

src/embed_tests/TestPyObject.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public void UnaryMinus_ThrowsOnBadType()
8282

8383
[Test]
8484
[Obsolete]
85+
[Ignore("Obsolote.")]
8586
public void GetAttrDefault_IgnoresAttributeErrorOnly()
8687
{
8788
var ob = new PyObjectTestMethods().ToPython();

src/runtime/Converter.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ private Converter()
3131
{
3232
}
3333

34+
/// <summary>
35+
/// Releases the cached enum wrappers. Must be called on shutdown while the
36+
/// Python runtime is still alive: the cache holds Python objects created in
37+
/// the current run, and if they survive into the next Initialize/Shutdown
38+
/// cycle their handles dangle and corrupt the interpreter heap.
39+
/// </summary>
40+
internal static void Reset()
41+
{
42+
foreach (var cached in _enumCache.Values)
43+
{
44+
cached.Dispose();
45+
}
46+
_enumCache.Clear();
47+
}
48+
3449
private static NumberFormatInfo nfi;
3550
private static Type objectType;
3651
private static Type stringType;
@@ -842,7 +857,8 @@ internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delega
842857
}
843858

844859
PythonEngine.Exec(code, null, locals);
845-
result = locals.GetItem("delegate").AsManagedObject(delegateType);
860+
using var delegateObj = locals.GetItem("delegate");
861+
result = delegateObj.AsManagedObject(delegateType);
846862

847863
return true;
848864
}

src/runtime/Runtime.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,19 @@ internal static void Initialize(bool initSigs = false)
128128
PyGILState_Ensure();
129129
}
130130

131+
// The CPython interpreter is not finalized on PythonEngine.Shutdown
132+
// (we never call Py_Finalize), so when pythonnet is re-initialized in
133+
// the same process the run counter from the previous, already
134+
// torn-down session is still stored in sys. Reusing it would make the
135+
// Finalizer treat objects leaked from that dead session as belonging
136+
// to the current one and decref their now-dangling handles, corrupting
137+
// the heap. We only keep the previous run when actually restoring
138+
// serialized state across an AppDomain reload, which is flagged by the
139+
// presence of the "clr_data" stash capsule; otherwise we start a fresh
140+
// run so stale objects are safely skipped on finalization.
131141
BorrowedReference pyRun = PySys_GetObject(RunSysPropName);
132-
if (pyRun != null)
142+
bool restoringStashedState = !PySys_GetObject("clr_data").IsNull;
143+
if (pyRun != null && restoringStashedState)
133144
{
134145
run = checked((int)PyLong_AsSignedSize_t(pyRun));
135146
}
@@ -258,6 +269,10 @@ internal static void Shutdown()
258269

259270
var state = PyGILState_Ensure();
260271

272+
// Release the cached enum wrappers before tearing the runtime down, so
273+
// their handles do not dangle into the next Initialize/Shutdown cycle.
274+
Converter.Reset();
275+
261276
if (!HostedInPython && !ProcessIsTerminating)
262277
{
263278
// avoid saving dead objects

src/runtime/Types/LookUpObject.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ internal static bool VerifyMethodRequirements(Type type)
4141
}
4242

4343
var key = Tuple.Create(type, requiredMethod);
44-
methodsByType.Add(key, method);
44+
// Use indexer assignment rather than Add: this static cache survives a
45+
// PythonEngine shutdown, so the same type can be reflected again in a
46+
// later Initialize/Shutdown cycle. Add would throw a duplicate-key
47+
// ArgumentException on re-reflection, and that exception thrown from
48+
// within the native tp_getattro callback corrupts the interpreter.
49+
methodsByType[key] = method;
4550
}
4651

4752
return true;

0 commit comments

Comments
 (0)