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
Intermittent 0xC0000005 (use-after-free) in Library's memory-monitor timer at disposal / process shutdown
Summary
PicoGK.Library starts a periodic memory-monitoring System.Threading.Timer in its constructor. The timer callback calls the native _nTotalMemUsage(LibHandle) via the managed nTotalMemUsage() → MonitorMemory() path. When a Library instance is disposed, the native handle is freed but the monitor timer is not reliably stopped before the free, so a timer tick that is queued or already executing on a thread-pool thread dereferences a freed LibHandle → STATUS_ACCESS_VIOLATION (0xC0000005), which is a fatal, process-terminating error in .NET.
It is intermittent and timing-dependent. It does not occur with a single long-lived Library (the common application case), but reproduces reliably-often in any process that creates and disposes multiple Library instances — most notably an xUnit/test process that constructs a fresh Library per test for isolation. The crash happens after the managed work has completed (all tests pass), during disposal/teardown.
Environment
PicoGK
2.2.0 (NuGet). Also observed on 2.0.0 — i.e. not a 2.2 regression; long-standing.
Runtime
.NET 9 (net9.0-windows), x64
OS
Windows x64 (self-hosted CI runner)
CPU
AMD Ryzen 9 (high core count); crash frequency rises under CPU contention, consistent with a timing race
Trigger workload
A test process that constructs + disposes many Library (new Library(voxelSizeMM)) instances in one process
Severity / impact
Process-fatal:0xC0000005 terminates the process; there is no managed exception to catch (it is a corrupted-state access violation on a background thread).
Correctness of results is unaffected — it strikes during disposal/teardown after work completes — but it makes any harness that exercises repeated Library lifecycles (test suites, batch tools) unable to exit cleanly / return success, and is a hard blocker for clean CI.
Exact crash signature (verbatim)
From the .NET runtime fatal-error handler (test host process), captured in CI:
Fatal error. 0xC0000005
at PicoGK.Library._nTotalMemUsage(PicoGK.LibHandle)
at PicoGK.Library.nTotalMemUsage()
at PicoGK.Library.MonitorMemory()
at PicoGK.Library.<.ctor>b__25_0(System.Object)
at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
at System.Threading.TimerQueueTimer.Fire(Boolean)
at System.Threading.TimerQueue.FireNextTimers()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
VSTest reports it as:
The active test run was aborted. Reason: Test host process crashed : Fatal error. 0xC0000005
The stack is identical on every occurrence.
Root-cause analysis
Reading the stack from the bottom up:
A thread-pool timer fires (PortableThreadPool → TimerQueue.FireNextTimers → TimerQueueTimer.Fire).
The timer callback is PicoGK.Library.<.ctor>b__25_0 — a lambda registered in the Library constructor. (The <.ctor>b__25_0 synthesized name means "the closure created inside Library's constructor"; i.e. the Library ctor does something like new Timer(MonitorMemory, …, period).)
It calls Library.MonitorMemory() → Library.nTotalMemUsage() → native Library._nTotalMemUsage(LibHandle).
The native call access-violates because the LibHandle it is passed has already been freed by the disposal of that Library instance.
Conclusion (hypothesis to verify against your source): the memory-monitor Timer created in Library's constructor outlives the native handle. Library.Dispose() (and/or the finalizer) frees the native library/handle but does not stop and join the monitor timer first, leaving a window where a queued or in-flight MonitorMemory tick runs against a freed handle. Creating/disposing many Library instances opens that window repeatedly, so the AV becomes likely. A single long-lived Library never disposes during normal run, so the window never opens — which is exactly the observed pattern.
Minimal reproduction
A standalone console app that only creates and disposes Library instances — no geometry needed, since the memory-monitor timer is the culprit:
usingPicoGK;// Repro: intermittent 0xC0000005 in PicoGK.Library's memory-monitor timer.//// Each `new Library(...)` starts the background memory-monitor timer// (Library ctor -> b__25_0). Disposing the Library frees the native handle;// if the timer is not stopped first, a queued/in-flight tick calls native// _nTotalMemUsage(LibHandle) on the freed handle -> access violation.// Repeated create/dispose opens that race window many times.constintiterations=300;for(inti=0;i<iterations;i++){usingvarlib=newLibrary(0.5f);// 0.5 mm voxel size; no geometry needed}Console.WriteLine($"Survived {iterations} Library create/dispose cycles on this run.");
How to run / observe:
dotnet run -c Release
Re-run a handful of times (or raise iterations, or run several instances in parallel to add CPU pressure). Intermittently the process terminates with the Fatal error. 0xC0000005 stack above instead of printing the "Survived …" line. It is a timing race, so it will not fail every run.
Frequency / evidence
Observed ~6 times on 2026-06-16 across CI runs of a ~4,500-test xUnit suite (each test uses its own Library instance), interleaved with clean passes — i.e. intermittent, roughly 40–60 % of full runs on the Ryzen 9 runner. Two surface variants, both the same timer stack:
crash after all tests completed → run reports Failed: 0 then aborts;
crash while a test was mid-execution → that one test is reported Failed: 1 (no assertion failure), then aborts.
What we ruled out
Not our managed code: the AV is entirely within PicoGK.Library's own timer callback → native call; our code never calls nTotalMemUsage/MonitorMemory.
Not a specific test: the crashing frame is the monitor timer, independent of which test is running; the "failed" test (when any) varies and shows no assertion failure.
Not version-specific: reproduces on both PicoGK 2.0.0 and 2.2.0.
Not present without lifecycle churn: single long-lived Library processes are clean.
Suggested fix (for your consideration)
In Library.Dispose() (and the finalizer path), stop and dispose the memory-monitor timer before freeing the native handle, and ensure no callback is in-flight — e.g. Timer.Dispose(WaitHandle) / await DisposeAsync() to block until any running tick completes, or a disposed-guard in MonitorMemory() that bails before touching the handle. (Equivalent: make _nTotalMemUsage a no-op once the handle is invalidated, under the same lock that guards disposal.)
Current workaround (downstream)
We tolerate the crash in CI: treat a non-zero dotnet test exit as success iff the console output contains Test host process crashedand there is no xUnit [FAIL] assertion marker (so genuine failures still fail). This is a band-aid for the false-red; the underlying UAF is what this report asks you to fix.
Happy to run any diagnostic build, a debug/Verbose Library build, or capture a full crash dump (procdump/DOTNET_DbgEnableMiniDump=1) if that would help confirm the timer-vs-handle disposal ordering.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Intermittent
0xC0000005(use-after-free) inLibrary's memory-monitor timer at disposal / process shutdownSummary
PicoGK.Librarystarts a periodic memory-monitoringSystem.Threading.Timerin its constructor. The timer callback calls the native_nTotalMemUsage(LibHandle)via the managednTotalMemUsage()→MonitorMemory()path. When aLibraryinstance is disposed, the native handle is freed but the monitor timer is not reliably stopped before the free, so a timer tick that is queued or already executing on a thread-pool thread dereferences a freedLibHandle→STATUS_ACCESS_VIOLATION (0xC0000005), which is a fatal, process-terminating error in .NET.It is intermittent and timing-dependent. It does not occur with a single long-lived
Library(the common application case), but reproduces reliably-often in any process that creates and disposes multipleLibraryinstances — most notably an xUnit/test process that constructs a freshLibraryper test for isolation. The crash happens after the managed work has completed (all tests pass), during disposal/teardown.Environment
net9.0-windows), x64Library(new Library(voxelSizeMM)) instances in one processSeverity / impact
0xC0000005terminates the process; there is no managed exception to catch (it is a corrupted-state access violation on a background thread).Librarylifecycles (test suites, batch tools) unable to exit cleanly / return success, and is a hard blocker for clean CI.Exact crash signature (verbatim)
From the .NET runtime fatal-error handler (test host process), captured in CI:
VSTest reports it as:
The stack is identical on every occurrence.
Root-cause analysis
Reading the stack from the bottom up:
PortableThreadPool→TimerQueue.FireNextTimers→TimerQueueTimer.Fire).PicoGK.Library.<.ctor>b__25_0— a lambda registered in theLibraryconstructor. (The<.ctor>b__25_0synthesized name means "the closure created insideLibrary's constructor"; i.e. theLibraryctor does something likenew Timer(MonitorMemory, …, period).)Library.MonitorMemory()→Library.nTotalMemUsage()→ nativeLibrary._nTotalMemUsage(LibHandle).LibHandleit is passed has already been freed by the disposal of thatLibraryinstance.Conclusion (hypothesis to verify against your source): the memory-monitor
Timercreated inLibrary's constructor outlives the native handle.Library.Dispose()(and/or the finalizer) frees the native library/handle but does not stop and join the monitor timer first, leaving a window where a queued or in-flightMonitorMemorytick runs against a freed handle. Creating/disposing manyLibraryinstances opens that window repeatedly, so the AV becomes likely. A single long-livedLibrarynever disposes during normal run, so the window never opens — which is exactly the observed pattern.Minimal reproduction
A standalone console app that only creates and disposes
Libraryinstances — no geometry needed, since the memory-monitor timer is the culprit:Repro.csprojProgram.csHow to run / observe:
Re-run a handful of times (or raise
iterations, or run several instances in parallel to add CPU pressure). Intermittently the process terminates with theFatal error. 0xC0000005stack above instead of printing the "Survived …" line. It is a timing race, so it will not fail every run.Frequency / evidence
Observed ~6 times on 2026-06-16 across CI runs of a ~4,500-test xUnit suite (each test uses its own
Libraryinstance), interleaved with clean passes — i.e. intermittent, roughly 40–60 % of full runs on the Ryzen 9 runner. Two surface variants, both the same timer stack:Failed: 0then aborts;Failed: 1(no assertion failure), then aborts.What we ruled out
PicoGK.Library's own timer callback → native call; our code never callsnTotalMemUsage/MonitorMemory.Libraryprocesses are clean.Suggested fix (for your consideration)
In
Library.Dispose()(and the finalizer path), stop and dispose the memory-monitor timer before freeing the native handle, and ensure no callback is in-flight — e.g.Timer.Dispose(WaitHandle)/await DisposeAsync()to block until any running tick completes, or a disposed-guard inMonitorMemory()that bails before touching the handle. (Equivalent: make_nTotalMemUsagea no-op once the handle is invalidated, under the same lock that guards disposal.)Current workaround (downstream)
We tolerate the crash in CI: treat a non-zero
dotnet testexit as success iff the console output containsTest host process crashedand there is no xUnit[FAIL]assertion marker (so genuine failures still fail). This is a band-aid for the false-red; the underlying UAF is what this report asks you to fix.Happy to run any diagnostic build, a debug/Verbose
Librarybuild, or capture a full crash dump (procdump/DOTNET_DbgEnableMiniDump=1) if that would help confirm the timer-vs-handle disposal ordering.All reactions