PSReadLine 2.4.5 crashes pwsh on durable console-input loss — #3744's retry guard is too short to help

未关闭
#5,197 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
冷清
技术栈
csharp, powershell
领域
cli

调研方向

Start with PSReadLine/ConsoleLib.cs, focusing on _TryIgnoreIOE and its callers ReadKey, KeyAvailable, and ReadKeyThreadProc. Use the suggested shared-console FreeConsole or abrupt-exit scenario to investigate durable console-input loss; done means exhausted failures do not terminate pwsh and the ReadLine session ends or reports a recoverable error while transient loss can recover.

由索引模型根据 Issue 内容生成。

描述

Needs-Triage :mag:
Prerequisites
  • Write a descriptive title.
  • Make sure you are able to repro it on the latest released version
  • Search the existing issues, especially the pinned issues.
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.

  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 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.

  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 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.

  2. 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

主要语言
C#
星标
4.4k
派生
341
PR 合并指标
30 天内没有已合并 PR

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

PowerShell/PSReadLine 的其他 Issue

查看 PowerShell/PSReadLine 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。