Prerequisites
Exception report
## Summary
`PSConsoleReadLine.ReadKeyThreadProc` terminated the whole `pwsh.exe` process with an unhandled
`System.InvalidOperationException` ("Cannot read keys when either application does not have a console
or when console input has been redirected"), **with the `_TryIgnoreIOE` mitigation from #3744 present
in the stack**.
This is not a duplicate of #3744 — it is evidence that the fix for #3744 is **incomplete by
construction**. That fix assumes the `InvalidOperationException` is a *transient* race (a
co-attached process exiting), and retries 10 times **with no delay**. When console input is
invalidated *durably* rather than momentarily, all 10 attempts fail within microseconds, the
exception is rethrown on a background thread with no top-level handler, and the process dies.
The user-visible failure is severe and gives no diagnostic: the terminal window keeps its last
painted frame, **stops accepting keyboard input entirely** (PSReadLine's reader thread is the thing
that consumes keystrokes), does not respond to close or minimize, and several minutes later the
process simply vanishes. No error is printed. No `Application Hang` (Event ID 1002) is logged,
because nothing hung — so anyone triaging this from Event Viewer is looking for the wrong class of
fault. Every other process on the machine is unaffected.
## Actual behaviour
`.NET Runtime` Event ID 1026:
Application: pwsh.exe
CoreCLR Version: 10.0.1126.37416
.NET Version: 10.0.11
Description: The process was terminated due to an unhandled exception.
Exception Info: System.InvalidOperationException: Cannot read keys when either application does not
have a console or when console input has been redirected. Try Console.Read.
at System.ConsolePal.ReadKey(Boolean intercept)
at Microsoft.PowerShell.Internal.VirtualTerminal._TryIgnoreIOE[T](Func`1 f)
at Microsoft.PowerShell.PSConsoleReadLine.ReadOneOrMoreKeys()
at Microsoft.PowerShell.PSConsoleReadLine.ReadKeyThreadProc()
at System.Threading.Thread.StartHelper.Callback(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
Paired `Application Error` Event ID 1000:
Faulting application name: pwsh.exe, version: 7.6.5.500, time stamp: 0x6a630000
Faulting module name: KERNELBASE.dll, version: 10.0.26100.8875, time stamp: 0xca32cd54
Exception code: 0xe0434352
Fault offset: 0x00000000000c1ada
Faulting process id: 0xBE4C
Faulting application path: C:\Program Files\PowerShell\7\pwsh.exe
Report Id: 2b8c89f0-5813-429f-b72f-5699da29aa42
A full user-mode crash dump was captured (9.3 MB) and is available on request.
## Expected behaviour
Two separate expectations, in priority order:
1. **An exhausted retry in `_TryIgnoreIOE` must not terminate the host process.** `ReadKeyThreadProc`
is a background thread with no top-level handler, so any escape is fatal and silent. Even when
the retries are legitimately exhausted, the correct outcome is to end the current `ReadLine`
session / surface a recoverable error — not to destroy the session and every child process
running under that console. Losing an entire working session (and, for console-hosted agents and
CI harnesses, everything running beneath it) to a keystroke-reader thread is disproportionate.
2. **The retry policy should cover a state change, not just a nanosecond race.** See below.
## Analysis — where the #3744 guard has a hole
Current implementation (`PSReadLine/ConsoleLib.cs`):
private static T _TryIgnoreIOE<T>(Func<T> f)
{
int triesLeft = 10;
while (true)
{
try
{
triesLeft--;
return f();
}
catch (InvalidOperationException)
{
if (triesLeft <= 0)
{
throw;
}
}
}
}
used as:
public ConsoleKeyInfo ReadKey() => _TryIgnoreIOE(() => _readKeyMethod.Value(true));
public bool KeyAvailable => _TryIgnoreIOE(() => Console.KeyAvailable);
Three defects:
1. **No backoff.** Ten immediate iterations of a tight loop complete in microseconds. That covers the
original #3744 race (.NET returning 0 records because a co-attached process was terminated
mid-read) but cannot cover a console state transition that takes milliseconds to settle. A short
delay between attempts — even 10 ms — would cover ~100 ms of instability for no practical cost on
a thread that is otherwise blocked waiting for a human.
2. **No distinction between transient and terminal.** If the console input handle has been
invalidated for good (`FreeConsole`, handle closed, input genuinely redirected), retrying 10 times
is futile by construction. The two cases need different handling: retry the race, gracefully end
the ReadLine session on the durable case.
3. **Rethrow is fatal and silent.** As in (1) of *Expected behaviour*. The user gets no message at
all — only an unresponsive window and, minutes later, a disappeared process.
## Steps to reproduce
I do not have a deterministic minimal repro, and I would rather say so than dress up a guess. What I
have is a measured timeline with a strong and mechanistically plausible trigger:
1. A console-hosted agent harness runs in a Windows Terminal `pwsh` session and spawns child `pwsh`
processes for tool calls; those children **share the parent's console**.
2. In one such child, `Connect-MgGraph` (`Microsoft.Graph.Authentication` 2.37.0) performed an
interactive Entra sign-in via `Azure.Identity`'s `InteractiveBrowserCredential` / WAM. The broker
dialog opened behind other windows and was cancelled, returning
`InteractiveBrowserCredential authentication failed: User canceled authentication.`
3. From approximately that moment, the parent console **accepted no keyboard input**. The window
still painted, but did not respond to typing, to close, or to minimize.
4. About 8 minutes later, `pwsh.exe` terminated with the unhandled exception above.
**Measured:** the exception, the stack, the fault code, the absence of any hang event, the timings,
and the version set below. **Inferred, not proven:** that the WAM/broker interactive flow attaching
to and detaching from the *shared* console is what invalidated the input handle. I note that this
mechanism is a direct match for the root cause described in #3744 — "if a second process attached to
the console is also waiting for input, and then is terminated, .NET gets back 0 records, and decides
to throw" — except that here the invalidation appears to persist, which is exactly why 10 immediate
retries do not save it.
A likely synthetic repro for maintainers: from a `pwsh` session with PSReadLine loaded, have a second
process attached to the same console take console input and then `FreeConsole` / exit abruptly while
PSReadLine's reader thread is blocked in `ReadKey`.
## Environment
| | |
|---|---|
| PSReadLine | **2.4.5** (`C:\program files\powershell\7\Modules\PSReadLine`, as shipped with PS 7.6.5) |
| PowerShell | 7.6.5 (`GitCommitId` 7.6.5), `pwsh.exe` 7.6.5.500 |
| .NET / CoreCLR | .NET 10.0.11 / CoreCLR 10.0.1126.37416 |
| OS | Windows 11 Enterprise 25H2, build **26200.9106** |
| Terminal | Windows Terminal 1.24.11911.0 |
| Faulting module | `KERNELBASE.dll` 10.0.26100.8875 |
| Third party in the trigger path | `Microsoft.Graph.Authentication` 2.37.0 → `Azure.Identity` `InteractiveBrowserCredential` (WAM) |
No `oh-my-posh`, `posh-git`, or `Terminal-Icons` in this session — the module set reported in
PowerShell/PowerShell#23979 is **not** required to hit this.
## Why this belongs here rather than in PowerShell/PowerShell
PowerShell/PowerShell#23979 was closed `Resolution-External`, and correctly so — every frame in the
stack below `Thread.StartHelper` belongs to PSReadLine, and the retry policy that decides whether
this crashes lives in `PSReadLine/ConsoleLib.cs`. The mitigation is here, so the gap in the
mitigation is here too.
Screenshot
No screenshot available, all pertinent information has been supplied.
Environment data
PS Version: 7.6.5
PS HostName: ConsoleHost (Windows Terminal)
PSReadLine Version: 2.4.5
PSReadLine EditMode: Windows
OS: 10.0.26100.1 (WinBuild.160101.0800)
BufferWidth: 179
BufferHeight: 52
Steps to reproduce
I do not have a deterministic minimal repro and would rather say so than dress up a guess.
What I have is a measured timeline with a mechanistically plausible trigger.
- A console-hosted agent harness runs in a Windows Terminal
pwsh session and spawns child
pwsh processes for tool calls. Those children SHARE the parent's console.
- In one such child,
Connect-MgGraph (Microsoft.Graph.Authentication 2.37.0) performed an
interactive Entra sign-in via Azure.Identity's InteractiveBrowserCredential / WAM. The broker
dialog opened behind other windows and was cancelled, returning:
InteractiveBrowserCredential authentication failed: User canceled authentication.
- From approximately that moment the parent console accepted NO keyboard input. The window still
painted, but did not respond to typing, to close, or to minimize.
- About 8 minutes later
pwsh.exe terminated with the unhandled exception below.
Suggested synthetic repro: from a pwsh session with PSReadLine loaded, have a second process
attached to the same console take console input and then FreeConsole / exit abruptly while
PSReadLine's reader thread is blocked in ReadKey.
Note this is the same root cause described in #3744 ("if a second process attached to the console is
also waiting for input, and then is terminated, .NET gets back 0 records, and decides to throw") --
except here the invalidation appears to PERSIST, which is exactly why 10 immediate retries cannot
recover from it.
Expected behavior
Two separate expectations, in priority order.
-
An exhausted retry in _TryIgnoreIOE must not terminate the host process. ReadKeyThreadProc
is a background thread with no top-level handler, so any escape is fatal and silent. Even when
the retries are legitimately exhausted, the correct outcome is to end the current ReadLine
session or surface a recoverable error -- not to destroy the session and every child process
running under that console. For console-hosted agents and CI harnesses, a keystroke-reader
thread takes down everything running beneath it.
-
The retry policy should cover a state change, not just a nanosecond race. _TryIgnoreIOE
retries 10 times with NO delay, so all ten attempts complete in microseconds. Even a 10 ms
delay between attempts would cover ~100 ms of console instability, at no practical cost on a
thread that is otherwise blocked waiting for a human.
Actual behavior
The process was terminated by an unhandled exception on PSReadLine's key-reader thread, with the
_TryIgnoreIOE mitigation from #3744 present in the stack -- i.e. the guard was in place and still
let the host die.
.NET Runtime, Event ID 1026:
Application: pwsh.exe
CoreCLR Version: 10.0.1126.37416
.NET Version: 10.0.11
Description: The process was terminated due to an unhandled exception.
Exception Info: System.InvalidOperationException: Cannot read keys when either application does not
have a console or when console input has been redirected. Try Console.Read.
at System.ConsolePal.ReadKey(Boolean intercept)
at Microsoft.PowerShell.Internal.VirtualTerminal._TryIgnoreIOE[T](Func`1 f)
at Microsoft.PowerShell.PSConsoleReadLine.ReadOneOrMoreKeys()
at Microsoft.PowerShell.PSConsoleReadLine.ReadKeyThreadProc()
at System.Threading.Thread.StartHelper.Callback(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
Paired Application Error, Event ID 1000:
Faulting application name: pwsh.exe, version: 7.6.5.500, time stamp: 0x6a630000
Faulting module name: KERNELBASE.dll, version: 10.0.26100.8875, time stamp: 0xca32cd54
Exception code: 0xe0434352
Fault offset: 0x00000000000c1ada
Report Id: 2b8c89f0-5813-429f-b72f-5699da29aa42
A full user-mode crash dump (9.3 MB) was captured and is available on request.
User-visible symptoms, which give no diagnostic at all: the window keeps its last painted frame,
stops accepting keyboard input entirely, ignores close and minimize, and several minutes later the
process simply vanishes. No error is printed. Critically, NO Application Hang (Event ID 1002) is
logged -- nothing hung -- so anyone triaging from Event Viewer is looking for the wrong fault class.
Every other process on the machine is unaffected
Prerequisites
Exception report
Screenshot
No screenshot available, all pertinent information has been supplied.
Environment data
Steps to reproduce
I do not have a deterministic minimal repro and would rather say so than dress up a guess.
What I have is a measured timeline with a mechanistically plausible trigger.
pwshsession and spawns childpwshprocesses for tool calls. Those children SHARE the parent's console.Connect-MgGraph(Microsoft.Graph.Authentication 2.37.0) performed aninteractive Entra sign-in via Azure.Identity's InteractiveBrowserCredential / WAM. The broker
dialog opened behind other windows and was cancelled, returning:
InteractiveBrowserCredential authentication failed: User canceled authentication.painted, but did not respond to typing, to close, or to minimize.
pwsh.exeterminated with the unhandled exception below.Suggested synthetic repro: from a
pwshsession with PSReadLine loaded, have a second processattached to the same console take console input and then FreeConsole / exit abruptly while
PSReadLine's reader thread is blocked in ReadKey.
Note this is the same root cause described in #3744 ("if a second process attached to the console is
also waiting for input, and then is terminated, .NET gets back 0 records, and decides to throw") --
except here the invalidation appears to PERSIST, which is exactly why 10 immediate retries cannot
recover from it.
Expected behavior
Two separate expectations, in priority order.
An exhausted retry in
_TryIgnoreIOEmust not terminate the host process.ReadKeyThreadProcis a background thread with no top-level handler, so any escape is fatal and silent. Even when
the retries are legitimately exhausted, the correct outcome is to end the current ReadLine
session or surface a recoverable error -- not to destroy the session and every child process
running under that console. For console-hosted agents and CI harnesses, a keystroke-reader
thread takes down everything running beneath it.
The retry policy should cover a state change, not just a nanosecond race.
_TryIgnoreIOEretries 10 times with NO delay, so all ten attempts complete in microseconds. Even a 10 ms
delay between attempts would cover ~100 ms of console instability, at no practical cost on a
thread that is otherwise blocked waiting for a human.
Actual behavior
The process was terminated by an unhandled exception on PSReadLine's key-reader thread, with the
_TryIgnoreIOEmitigation from #3744 present in the stack -- i.e. the guard was in place and stilllet the host die.
.NET Runtime, Event ID 1026:
Application: pwsh.exe
CoreCLR Version: 10.0.1126.37416
.NET Version: 10.0.11
Description: The process was terminated due to an unhandled exception.
Exception Info: System.InvalidOperationException: Cannot read keys when either application does not
have a console or when console input has been redirected. Try Console.Read.
at System.ConsolePal.ReadKey(Boolean intercept)
at Microsoft.PowerShell.Internal.VirtualTerminal._TryIgnoreIOE[T](Func`1 f)
at Microsoft.PowerShell.PSConsoleReadLine.ReadOneOrMoreKeys()
at Microsoft.PowerShell.PSConsoleReadLine.ReadKeyThreadProc()
at System.Threading.Thread.StartHelper.Callback(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
Paired Application Error, Event ID 1000:
Faulting application name: pwsh.exe, version: 7.6.5.500, time stamp: 0x6a630000
Faulting module name: KERNELBASE.dll, version: 10.0.26100.8875, time stamp: 0xca32cd54
Exception code: 0xe0434352
Fault offset: 0x00000000000c1ada
Report Id: 2b8c89f0-5813-429f-b72f-5699da29aa42
A full user-mode crash dump (9.3 MB) was captured and is available on request.
User-visible symptoms, which give no diagnostic at all: the window keeps its last painted frame,
stops accepting keyboard input entirely, ignores close and minimize, and several minutes later the
process simply vanishes. No error is printed. Critically, NO Application Hang (Event ID 1002) is
logged -- nothing hung -- so anyone triaging from Event Viewer is looking for the wrong fault class.
Every other process on the machine is unaffected