8000 Fix the NREs when writing to console from multiple threads by kborowinski · Pull Request #25440 · PowerShell/PowerShell · GitHub
[go: up one dir, main page]

Skip to content

Fix the NREs when writing to console from multiple threads #25440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2025

Conversation

kborowinski
Copy link
Contributor

PR Summary

Fix #21021

(Reopening this small PR on a new dev branch after closure due to the GitHub issue confirmed by support.)

This PR adds additional locks to prevent NullReferenceException when writing to the console from multiple threads. By ensuring proper synchronization, it improves the stability and reliability of concurrent console output in PowerShell.

PR Context

The issue #21021 was caused by race conditions where multiple threads attempted to write to the console simultaneously, leading to potential NullReferenceExceptions. This PR introduces additional locking mechanisms to prevent such occurrences and ensure thread-safe console access.

I would like to thank @iSazonov for finding the cause and proposing the solution.

Visuals

On the left with the fix, on the right current state:

Animation

PR Checklist

@iSazonov iSazonov added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Apr 25, 2025
@kborowinski
Copy link
Contributor Author

@iSazonov Could you re-add the backport consider labels? Or it does not matter.

@iSazonov

This comment was marked as outdated.

This comment was marked as outdated.

@iSazonov iSazonov added WG-Reviewed A Working Group has reviewed this and made a recommendation BackPort-7.4.x-Consider BackPort-7.5.x-Consider labels Apr 25, 2025
@iSazonov iSazonov merged commit 658a8e4 into PowerShell:master Apr 25, 2025
39 of 40 checks passed
@iSazonov
Copy link
Collaborator

@kborowinski Thanks for your contribution!

@TravisEz13
Copy link
Member

@PowerShell/powershell-maintainers triage decision:
For 7.4, I don't think a fix that doesn't affect the actual script results makes the bar for an LTS. The regression risk would be higher than the potential gains here.

@kborowinski
Copy link
Contributor Author

@TravisEz13

I understand the decision not to backport this PR to the LTS version, but I would like to clarify that the actual script results are affected in PS 7.4.X and up.

I first reported this issue on the 7Zip4PowerShell repo, where the unpacking process was crashing mid-task.
I proposed a PR that was merged to implement a workaround that swallows NREs coming from progress pane:

try {
    worker.Progress.StatusDescription = "Finished";
    worker.Progress.RecordType = ProgressRecordType.Completed;
    WriteProgress(worker.Progress);
} catch (NullReferenceException) {
    // Possible bug in PowerShell 7.4.0 leading to a null reference exception being thrown on ProgressPane completion
    // This is not happening on PowerShell 5.1
}

This issue affects all scripts that write to the console from multiple threads, leading to potential crashes in the LTS version of PowerShell. Affected users are forced to disable the progress bar either globally or per script.

pwshBot pushed a commit to pwshBot/PowerShell that referenced this pull request May 3, 2025
…l#25440)

The WriteImpl() method should always be called within a lock on _instanceLock to ensure thread safety.
pwshBot pushed a commit to pwshBot/PowerShell that referenced this pull request May 3, 2025
…l#25440)

The WriteImpl() method should always be called within a lock on _instanceLock to ensure thread safety.
@TravisEz13
Copy link
Member

@kborowinski I had the bot open PRs for the backports and added your comments. These still need to be triaged, but we are moving over to a new process that helps us track all the information we need.

@TravisEz13
Copy link
Member
  1. @kborowinski It would be good if you know exactly when it was introduced, that will help us with our decision.

@kborowinski
Copy link
Contributor Author

@TravisEz13 In comment on the original issue @iSazonov pinpointed it to PR #13758 PowerShell 7.2.0-preview.2 and #15864 PowerShell 7.2.0-preview.9 when PSAnsiRendering was made stable.

@daxian-dbw
Copy link
Member
daxian-dbw commented May 20, 2025

Root Cause

I debugged the code and found that the race condition was caused by ConsoleHostUserInterface.PreWrite calls _progPane?.Hide() without locking the _instanceLock.

The NRE happens because _savedRegion gets set to null while the Hide() method is running. However, _savedRegion is set to null only in the Hide() method, and that means at least two Hide() are running in parallel, which is unexpected.

Looking closely, Hide() has 5 call sites, and 3 of them are guarded by lock (_instanceLock). The other 2 un-guarded call sites are ConsoleHostUserInterface.PreWrite() and ConsoleHostUserInterface.PreRead(). The PreWrite() method gets called in WriteToCosnole method, which is the most low-level method called by all Write-* commands, including Write-Verbose.

The root cause becomes clear give the above -- calling Write-Verbose in the middle of on-going Write-Progress potentially causes the Hide() method to be called in parallel, which corrupts the state of the ProgressPane.

So, the potential problem still exists in CosnoleHostUserInterface.PreRead(), ConsoleHostUserInterface.PostWrite(), and ConsoleHostUserInterface.PostRead(). The latter 2 call _progPane?.Show() without locking _instanceLock.

Thoughts on this fix

The safest and complete fix would be adding lock (_instanceLock) to PreWrite(), PreRead(), PostWrite(), and PostRead(), so as to make all calls to Hide and Show guarded by the same lock.

However, after discussing with @SeeminglyScience and thinking about this more, we decide to not make further changes to the original fix. Here is why:

  1. By looking into the code more closely, it seems all Write-* actions (including $host.UI.Write*) that can be triggered by a user go through WriteImpl, which has been guarded by the original fix. Other calls to WriteToCosnole outside of WriteImpl are for reading input, prompting user with choices, and etc., not related to a Write-* action from user. So, it feels to me that guard WriteImpl is sufficient for the cases where a race condition could happen.

  2. Comparatively, even though adding locks to PreWrite and PostWrite would be a safer and more complete fix, it's not really needed practically in cases where WriteToConsole is called because of reading input or prompting users. Furthermore, it will result in acquiring locks twice in the WriteToConsole method, which will make it less performant in general.

  3. Regarding PreRead and PostRead, they are used only when reading input, and it's unlikely to happen while progress rendering is ongoing. So, again, even though adding locks there would be a complete fix, it seems unnecessary practically and will degrade the performance for sure.

@iSazonov
Copy link
Collaborator

The safest and complete fix would be adding lock (_instanceLock) to PreWrite(), PreRead(), PostWrite(), and PostRead(), so as to make call calls to Hide and Show guarded by the same lock.

@daxian-dbw Thank you for your investigations! The reason I didn't dare to do this was for fear of a deadlock. There are too many combinations. Even a change in one place in the future could potentially open up scenarios impossible today.
Steve and I had a short exchange about this a few years ago with the conclusion to wait for feedback. Now that such report has been received, we have corrected the specific scenario. If there are more complex scenarios with problems, then it may be more correct not to saturate the code with locks, but to change the approach more fundamentally.

@daxian-dbw
Copy link
Member

@SeeminglyScience did look into the feasibility to make the progress rendering lock-less, using atomic primitives. But it turned out to be a too big effort and too low priority for the team to commit on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Backport-7.4.x-Migrated Backport-7.5.x-Migrated CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log WG-Reviewed A Working Group has reviewed this and made a recommendation
Projects
Development

Successfully merging this pull request may close these issues.

Occasional NullReferenceException in Microsoft.PowerShell.ProgresPane.Hide() when updating multiple progress bars on PS 7.4.0
4 participants
0