From f71a02ec087550a27ecbee3aecd1a480b1ecea28 Mon Sep 17 00:00:00 2001 From: KirtiRamchandani Date: Sun, 24 May 2026 12:49:22 +0530 Subject: [PATCH] Avoid mutable ChildJobs enumeration in PSTaskJob --- .../engine/hostifaces/PSTask.cs | 11 ++++--- .../Foreach-Object-Parallel.Tests.ps1 | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/hostifaces/PSTask.cs b/src/System.Management.Automation/engine/hostifaces/PSTask.cs index 56bdabbff14..74a0a222976 100644 --- a/src/System.Management.Automation/engine/hostifaces/PSTask.cs +++ b/src/System.Management.Automation/engine/hostifaces/PSTask.cs @@ -998,6 +998,7 @@ public sealed class PSTaskJob : Job #region Members private readonly PSTaskPool _taskPool; + private readonly List _taskChildJobs; private bool _isOpen; private bool _stopSignaled; @@ -1031,6 +1032,7 @@ internal PSTaskJob( bool useNewRunspace) : base(command, string.Empty) { _taskPool = new PSTaskPool(throttleLimit, useNewRunspace); + _taskChildJobs = new List(); _isOpen = true; PSJobTypeName = nameof(PSTaskJob); @@ -1056,7 +1058,7 @@ public override bool HasMoreData { get { - foreach (var childJob in ChildJobs) + foreach (var childJob in _taskChildJobs) { if (childJob.HasMoreData) { @@ -1119,6 +1121,7 @@ internal bool AddJob(PSTaskChildJob childJob) } ChildJobs.Add(childJob); + _taskChildJobs.Add(childJob); return true; } @@ -1137,9 +1140,9 @@ internal void Start() System.Threading.ThreadPool.QueueUserWorkItem( (_) => { - foreach (var childJob in ChildJobs) + foreach (var childJob in _taskChildJobs) { - _taskPool.Add((PSTaskChildJob)childJob); + _taskPool.Add(childJob); } _taskPool.Close(); @@ -1162,7 +1165,7 @@ private void HandleTaskPoolComplete(object sender, EventArgs args) // 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 _taskChildJobs) { if (childJob.JobStateInfo.State != JobState.Completed) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 index de384c6ad78..2cf25a2124c 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 @@ -486,6 +486,35 @@ Describe 'ForEach-Object -Parallel -AsJob Basic Tests' -Tags 'CI' { $job | Wait-Job | Remove-Job } + It 'Does not crash when the ChildJobs collection is mutated while a parallel job starts' { + $testScript = Join-Path $TestDrive 'Mutate-ChildJobs.ps1' + $stdoutPath = Join-Path $TestDrive 'stdout.txt' + $stderrPath = Join-Path $TestDrive 'stderr.txt' + Set-Content -LiteralPath $testScript -Value @' +$job = 0..20 | ForEach-Object -AsJob -Parallel { + Start-Sleep -Milliseconds 300 + $_ +} -ThrottleLimit 2 + +Start-Sleep -Milliseconds 100 +$job.ChildJobs.RemoveAt(15) + +$job | Wait-Job -Timeout 30 | Out-Null +$job | Remove-Job -Force +'@ + + $powershell = Join-Path -Path $PSHOME -ChildPath "pwsh" + $process = Start-Process -FilePath $powershell -ArgumentList @( + '-NoLogo' + '-NoProfile' + '-NonInteractive' + '-File' + $testScript + ) -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru + + $process.ExitCode | Should -Be 0 + } + It 'Verifies dollar underbar variable' { $expected = 1..10