-
Notifications
You must be signed in to change notification settings - Fork 8k
Description
This was found while investigating a PowerShell class static/instance method concurrency bug.
Background
When invoking a script block using InvokeWithPipe, if the script block is bound to a different Runspace (e.g. created in a different Runspace), then the script block will be marshaled to that Runspace using the EventManager and is supposed to be executed by the main pipeline thread of that Runspace.
This is how it's done:
- The thread that is calling
InvokeWithPipefinds that the script block is bound to another Runsapce and needs to run in that Runspace. (code here) - The thread queues an event on the
EventManagerof the target Runspace, wishing the main pipeline thread of the target Runspace would pick up the event and run the script block. (code here) - The thread waits for the main pipeline thread of the target Runspace to finish running the script block. (code here)
Issues
The problem is that, to avoid a possible hang, the thread, which waits for the main pipeline thread of the target Runspace, will pick up the event and execute it (not the required main pipeline thread) after waiting for 250 mSec (See the code here). This will result in 2 threads running in the same Runspace and changing its state simultaneously. This would cause:
- Deadlock.
- Runspace state corruption and process crash.
Repro Steps
Deadlock
Both arguments for '-sb' and '-arg' are script blocks that are created in the powershell console session. So when bar runs $sb.InvokeReturnAsIs($arg) in a new Runspace, it needs to marshal it back to the powershell console session. This is what happens:
- The new Runspace thread (aka. requesting thread) cannot execute the script block because it has to run in the powershell console session (aka. target Runspace) which is bound with the script block, so it queues an event for the pipeline thread of the target Runspace to run the script block;
- However, the target Runspace is completely unresponsive because it’s blocked on
$ps.Invoke(); - So, after 250ms, the requesting thread go ahead to process the event itself to run the script block. Note that – an event is now executing;
- Again, the requesting thread finds it cannot run
$argand goes back to step (1). However, this time when it comes to step (3), an event is already executing. Sothis.ProcessPendingActions()will immediately return, and the requesting thread will be stuck in the while loop.
## The deadlock happens on PowerShell Core RC builds
$ps = [powershell]::Create()
$ps.AddScript('function bar { param([scriptblock]$sb, [scriptblock]$arg) $sb.InvokeReturnAsIs($arg) }').Invoke()
$ps.Commands.Clear()
$ps.AddCommand("bar").AddParameter('sb', {param([scriptblock] $s) $s.InvokeReturnAsIs()}).AddParameter('arg', {[Console]::WriteLine("blah")}) > $null
$ps.Invoke()Runspace state corruption and process crash
Note: this doesn't repro on latest PowerShell Core anymore because we have fixed the PowerShell class static method to not route method execution to other Runspaces incorrectly. But the underlying problem in
EventManageris still there. You can run this repro in Windows PowerShell v5.1 to see the results.
This repro creates a script DoInvokeInParallel.ps1 that dot-sources an Invoker.ps1 file which defines a class with a static method. Run DoInvokeInParallel.ps1 in multiple Runspaces via a RunspacePool to use the class static method concurrently.
Invoker.ps1 file
class Invoker
{
static [object[]] Invoke(
[scriptblock] $scriptToInvoke,
[object[]] $args)
{
return $scriptToInvoke.Invoke($args)
}
}DoInvokeInParallel.ps1 file
$invokerPath = Join-Path $PSScriptRoot Invoker.ps1
. $invokerPath
$rsp = [runspacefactory]::CreateRunspacePool(1, 10, $host)
$rsp.Open()
$scriptTemplate = @'
. {0}
1..100 | foreach {{
$results = [Invoker]::Invoke({{ "RS_{1} Loop $_ `r`n" }}, $null)
Write-Output $results
}}
'@
class Task
{
[powershell] $powershell
[System.IAsyncResult] $Async
}
$tasks = @()
1..10 | foreach {
$task = [Task]::new()
$script = $scriptTemplate -f $invokerPath,$_
$task.powershell = [powershell]::Create()
$null = $task.powershell.AddScript($script)
$task.powershell.RunspacePool = $rsp
$task.Async = $task.powershell.BeginInvoke()
$tasks += $task
}
foreach ($task in $tasks)
{
$results = $task.powershell.EndInvoke($task.Async)
Write-Host $results
Write-Host $task.powershell.Streams.Error
$task.powershell.Dispose()
}
$rsp.Dispose()Run DoInvokeInParallel.ps1. The result is multiple "invalid session state" asserts if you are using a debug flavor Windows PowerShell. Eventually, the process will crash.