Repository navigation
Process crashes with NullReferenceException in RunspacePoolInternal.DestroyRunspace when an open RunspacePool's cleanup timer fires during exit #28182
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version. I reproduced it on 7.6.2. The code involved (
RunspacePoolInternal.CleanupCallback,DestroyRunspaceandLocalRunspace.DoCloseHelper) is the same in v7.4.13, v7.5.4, v7.6.6 and master. - Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
If a process exits while one of its local RunspacePools is still open, and that pool's idle-cleanup timer fires during shutdown, the process crashes with an unhandled NullReferenceException.
Save this as pool-exit.ps1. It shortens CleanupInterval from the 15-minute default to 1 second so the timer fires close to exit:
param([int]$ExitAfterMs = 920)
$pool = [runspacefactory]::CreateRunspacePool(1, 10)
$pool.CleanupInterval = [timespan]::FromSeconds(1) # default is 15 minutes
$pool.Open()
$h = foreach ($i in 1..8) {
$ps = [powershell]::Create(); $ps.RunspacePool = $pool
[void]$ps.AddScript('Start-Sleep -Milliseconds 50')
[pscustomobject]@{ PS = $ps; H = $ps.BeginInvoke() }
}
foreach ($x in $h) { [void]$x.PS.EndInvoke($x.H); $x.PS.Dispose() }
Start-Sleep -Milliseconds $ExitAfterMs # exit just as the cleanup timer fires
# The pool is intentionally not disposed. A long-lived module-level pool behaves the same way.Run it in a loop. It is timing dependent, so vary the exit delay a little:
for i in $(seq 1 40); do pwsh -NoProfile -File pool-exit.ps1 -ExitAfterMs $((880 + (i % 8) * 20)) || echo "crash on run $i"; doneIn my tests:
- 9 of 40 runs crashed with this script.
- 6 of 30 runs crashed with a variant that runs
Get-Commandin the pool. - When the timer fires well before exit (exit at 3.5 s), 0 of 10 runs crashed.
- When
$pool.Dispose()is called before exit, 0 of 30 runs crashed.
Expected behavior
pwsh exits cleanly, with exit code 0.
Actual behavior
pwsh crashes during shutdown with an unhandled exception on a thread-pool timer thread. The exit code is 134 on macOS and -532462766 (0xE0434352) on Windows.
How we found it: maester365/maester CI on GitHub Actions (windows-2025). Our Pester suite ran PSScriptAnalyzer's tests about 15 minutes before the end of the suite. PSScriptAnalyzer's CommandInfoCache creates RunspaceFactory.CreateRunspacePool(1, 10) and never disposes it. All tests passed, then pwsh crashed on exit 15 minutes and 1 second after the PSScriptAnalyzer tests started. That matches the default CleanupInterval. It is intermittent because it only happens when the exit falls inside that window. Any module that keeps a pool open for the life of the process can trigger it.
Error details
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at System.Management.Automation.Runspaces.Internal.RunspacePoolInternal.DestroyRunspace(Runspace runspace)
at System.Management.Automation.Runspaces.Internal.RunspacePoolInternal.CleanupCallback(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.TimerQueue.FireNextTimers()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()Cause, from reading the source:
- When the primary runspace closes,
LocalRunspace.DoCloseHelper(withcloseAllOpenRunspaces) disposes everyOpenedrunspace inRunspace.RunspaceList. That includes idle runspaces still in aRunspacePool's internalpoolstack. The pool is not told: it staysOpenedand_cleanupTimerstays armed. CleanupCallbackonly returns early when the pool isClosing. Otherwise, whiletotalRunspaces > minPoolSz, it pops an idle runspace and callsDestroyRunspace.DestroyRunspacerunsrunspace.Events.ForwardEvent -= OnRunspaceForwardEvent.LocalRunspace.Eventsreturnsnullonce the runspace is closed (GetExecutionContextisnull), so this throws. The exception escapes aTimercallback and terminates the process.
Possible fixes (any one of these would help):
- In
DestroyRunspace, handle runspaces that are already closed: userunspace.Events?.ForwardEvent, and skipClose()/Dispose()if the runspace is already closed. - In
CleanupCallback, skip popped runspaces whoseRunspaceStateInfo.Stateis notOpened, and catch exceptions so a timer callback can never bring down the process. - When disposing all open runspaces at shutdown, also stop the cleanup timers of the pools that own them.
Environment data
Name Value
---- -----
PSVersion 7.6.2
PSEdition Core
GitCommitId 7.6.2
OS macOS 27.0.1
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.4
SerializationVersion 1.1.0.1
WSManStackVersion 3.0Also seen on GitHub Actions windows-2025 (image 20260925.250.1) with the preinstalled pwsh 7.x.
Visuals
No response
Activity