Skip to content

Avoid mutable ChildJobs enumeration in PSTaskJob#27512

Open
KirtiRamchandani wants to merge 2 commits into
PowerShell:masterfrom
KirtiRamchandani:fix/pstaskjob-childjobs-crash
Open

Avoid mutable ChildJobs enumeration in PSTaskJob#27512
KirtiRamchandani wants to merge 2 commits into
PowerShell:masterfrom
KirtiRamchandani:fix/pstaskjob-childjobs-crash

Conversation

@KirtiRamchandani
Copy link
Copy Markdown

@KirtiRamchandani KirtiRamchandani commented May 24, 2026

PR Summary

Avoids an unhandled process-terminating exception when PSTaskJob.ChildJobs is mutated while a ForEach-Object -Parallel -AsJob job is starting.

PR Context

Fix #26160.

PSTaskJob.Start() currently enumerates the public ChildJobs list on a ThreadPool callback. That list is externally mutable, so removing an item can invalidate the enumerator and let the exception escape the ThreadPool callback. This change snapshots the authoritative ChildJobs list at the internal enumeration points, so external mutations do not invalidate active enumerators and the public collection remains the source of truth.

PR Checklist

  • PR has a meaningful title
  • Summarized changes
  • This PR is focused on one issue
  • Make sure all .h, .cpp, .cs, .ps1 and .psm1 files have the correct copyright header
  • This PR is ready to merge and is not work-in-progress
  • Breaking change: No
  • User-facing change: Not required
  • Tests added/updated

Validation

  • Start-PSBuild -PSModuleRestore -UseNuGetOrg built pwsh.exe. The Windows PowerShell 5.1 build wrapper emitted post-build ConvertFrom-Json -Depth compatibility warnings after build output was produced.
  • Start-PSPester -Path test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 -UseNuGetOrg -ThrowOnFailure -Terse -SkipTestToolBuild passed: 62 passed, 0 failed.

@KirtiRamchandani KirtiRamchandani marked this pull request as ready for review May 24, 2026 07:30
@KirtiRamchandani KirtiRamchandani requested a review from a team as a code owner May 24, 2026 07:30
Copilot AI review requested due to automatic review settings May 24, 2026 07:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds regression coverage and internal changes to prevent crashes when PSTaskJob.ChildJobs is mutated while ForEach-Object -Parallel -AsJob is starting.

Changes:

  • Added a Pester test that reproduces a crash by mutating ChildJobs during job startup.
  • Introduced an internal _taskChildJobs list and switched several enumerations from ChildJobs to _taskChildJobs to avoid iteration over a mutating collection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 Adds a regression test that mutates ChildJobs during parallel job startup and asserts the process does not crash.
src/System.Management.Automation/engine/hostifaces/PSTask.cs Adds an internal child-job list and uses it for enumeration to reduce crash risk from ChildJobs mutations.
Comments suppressed due to low confidence (1)

src/System.Management.Automation/engine/hostifaces/PSTask.cs:1067

  • Switching HasMoreData and completion logic to _taskChildJobs can make PSTaskJob observably inconsistent when consumers mutate ChildJobs (which this PR’s test explicitly does). Removals from ChildJobs are not reflected in _taskChildJobs, so HasMoreData / final-state evaluation may continue to consider jobs that the consumer removed (and may no longer expect to affect the parent job). To keep behavior consistent, consider iterating over a snapshot of the authoritative ChildJobs collection (e.g., ChildJobs.ToArray()) rather than a separate list, or ensure _taskChildJobs is kept in sync with ChildJobs mutations (including removals).
                foreach (var childJob in _taskChildJobs)
                {
                    if (childJob.HasMoreData)
                    {
                        return true;
                    }
                }

Comment on lines +1000 to +1001
private readonly PSTaskPool _taskPool;
private readonly List<PSTaskChildJob> _taskChildJobs;
Comment on lines +1034 to +1035
_taskPool = new PSTaskPool(throttleLimit, useNewRunspace);
_taskChildJobs = new List<PSTaskChildJob>();
Comment on lines +1123 to +1124
ChildJobs.Add(childJob);
_taskChildJobs.Add(childJob);
Comment on lines 1140 to 1146
System.Threading.ThreadPool.QueueUserWorkItem(
(_) =>
{
foreach (var childJob in ChildJobs)
foreach (var childJob in _taskChildJobs)
{
_taskPool.Add((PSTaskChildJob)childJob);
_taskPool.Add(childJob);
}
Comment on lines 1168 to 1171
foreach (var childJob in _taskChildJobs)
{
if (childJob.JobStateInfo.State != JobState.Completed)
{
$_
} -ThrottleLimit 2

Start-Sleep -Milliseconds 100
@KirtiRamchandani
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@KirtiRamchandani KirtiRamchandani requested a review from Copilot May 24, 2026 13:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

$job | Remove-Job -Force
'@

$powershell = Join-Path -Path $PSHOME -ChildPath "pwsh"
'@

$powershell = Join-Path -Path $PSHOME -ChildPath "pwsh"
$process = Start-Process -FilePath $powershell -ArgumentList @(
Comment on lines +522 to +523
) -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru

Comment on lines +1060 to 1062
foreach (var childJob in ChildJobs.ToArray())
{
if (childJob.HasMoreData)
Comment on lines +1060 to 1061
foreach (var childJob in ChildJobs.ToArray())
{
(_) =>
{
foreach (var childJob in ChildJobs)
foreach (PSTaskChildJob childJob in ChildJobs.ToArray())
// Final state will be 'Complete', only if all child jobs completed successfully.
JobState finalState = JobState.Completed;
foreach (var childJob in ChildJobs)
foreach (var childJob in ChildJobs.ToArray())
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.

Mutating PSTaskJob.ChildJobs from ForEach-Object -Parallel -AsJob throws unhandled exception and crashes PowerShell

2 participants