Skip to content

Fix concurrent-import crash in Run.Parallel (thread-unsafe verb patch)#2901

Merged
nohwnd merged 2 commits into
mainfrom
nohwnd-fix-parallel-verbspatcher-race
Jul 18, 2026
Merged

Fix concurrent-import crash in Run.Parallel (thread-unsafe verb patch)#2901
nohwnd merged 2 commits into
mainfrom
nohwnd-fix-parallel-verbspatcher-race

Conversation

@nohwnd

@nohwnd nohwnd commented Jul 17, 2026

Copy link
Copy Markdown
Member

Run.Parallel runs every test file in its own ForEach-Object -Parallel runspace, but those runspaces share one process and the same loaded assemblies. On import Pester calls [Pester.VerbsPatcher]::AllowShouldVerb(...), which reflects into PowerShell's process-global System.Management.Automation.Verbs.s_validVerbs dictionary and does validVerbs["Should"] = true (plus a background task that removes it after 5 seconds).

Dictionary<TKey,TValue> is not thread-safe, so when several workers Import-Module Pester at the same time the concurrent writes tear the internal bucket array and throw:

Exception calling "AllowShouldVerb": "Index was outside the bounds of the array."

That kills the parallel Invoke-Pester, no [Pester.Run] object comes back, and tst/Pester.RSpec.Parallel.ts.ps1 then fails on Windows PS7 with follow-on errors like property TotalCount cannot be found or property Containers cannot be found. It is Windows-only and intermittent, but it is not flakiness, it is an unsynchronized write to a shared dictionary, and it reproduces on main.

The fix is in VerbsPatcher.cs only. Pester.dll loads once per process, so a static lock is shared across all runspaces. I wrap both the insert and the delayed removal in lock (s_lock) and guard them with ContainsKey, so the first import mutates the dictionary and the rest are no-ops. No await inside the lock.

Not Dictionary.TryAdd, it does not exist on the net462 target. Not a bare retry/catch, a torn dictionary is permanently corrupt, not transiently retryable.

Verified: build.ps1 -Clean is clean, test.ps1 -File tst/Pester.RSpec.Parallel.ts.ps1 -SkipPTests passes (22), and 1..8 | ForEach-Object -Parallel { Import-Module ./bin/Pester.psd1 } -ThrottleLimit 8 no longer crashes.

VerbsPatcher.AllowShouldVerb reaches into PowerShell's process-global
System.Management.Automation.Verbs verb dictionary and mutates it. That
Dictionary is not thread-safe, so parallel worker Import-Module calls in
Run.Parallel could corrupt it and throw IndexOutOfRange in set_Item.

Serialize both the insert and the delayed removal behind a process-global
lock and guard the insert with a ContainsKey check so only the first import
mutates the shared dictionary.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nohwnd nohwnd added this to the 6.1.0 milestone Jul 17, 2026
Reading the shared Dictionary with an unlocked ContainsKey before taking
the lock would race with the guarded insert (a resize could tear the
bucket array during a concurrent read), so document that the lock-only
form is intentional and that a correct fast path would need a separate
volatile flag.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@nohwnd
nohwnd merged commit 8b05c3d into main Jul 18, 2026
13 checks passed
@nohwnd
nohwnd deleted the nohwnd-fix-parallel-verbspatcher-race branch July 18, 2026 08:23
nohwnd added a commit that referenced this pull request Jul 18, 2026
#2901) (#2903)

* Fix concurrent-import crash in Run.Parallel (thread-unsafe verb patch)

VerbsPatcher.AllowShouldVerb reaches into PowerShell's process-global
System.Management.Automation.Verbs verb dictionary and mutates it. That
Dictionary is not thread-safe, so parallel worker Import-Module calls in
Run.Parallel could corrupt it and throw IndexOutOfRange in set_Item.

Serialize both the insert and the delayed removal behind a process-global
lock and guard the insert with a ContainsKey check so only the first import
mutates the shared dictionary.



* Explain why AllowShouldVerb does not double-check outside the lock

Reading the shared Dictionary with an unlocked ContainsKey before taking
the lock would race with the guarded insert (a resize could tear the
bucket array during a concurrent read), so document that the lock-only
form is intentional and that a correct fast path would need a separate
volatile flag.



---------


(cherry picked from commit 8b05c3d)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

1 participant