0%

PowerShell 7.6.6 中文环境启动崩溃:一行中文文案引发的 FailFast

PowerShell 7.6.6 中文环境启动崩溃:一行中文文案引发的 FailFast

TL;DR:这不是 Windows 更新,也不是配置损坏。是 PowerShell 自带的”有新版本”提醒在简体中文(zh-Hans)下的格式化 bug:代码假设提醒文案第一行最长,中文文案第三行反而最长,算出 PadRight(-17),.NET 直接抛异常,进程在启动阶段 FailFast。7.6.7 发布满 7 天后提醒到期,所以某天早上突然开始崩。

临时修复:设置用户环境变量 POWERSHELL_UPDATECHECK=Off,关闭更新提醒。升级到 7.6.7 不能解决。

一、现象

环境:Windows 11 + PowerShell 7.6.6(系统 UI 语言为简体中文)。

某天开始,打开 PowerShell 7 直接进程终止,窗口只剩一段报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PowerShell 7.6.6
Process terminated.
totalWidth ('-17') must be a non-negative value. (Parameter 'totalWidth')
Actual value was -17.

at System.Environment.FailFast(System.String, System.Exception)
at Microsoft.PowerShell.UnmanagedPSEntry.Start(System.String[], Int32)
at Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])

System.ArgumentOutOfRangeException: totalWidth ('-17') must be a non-negative value. (Parameter 'totalWidth')
Actual value was -17.
at System.ArgumentOutOfRangeException.ThrowNegative[T](T value, String paramName)
at System.String.PadRight(Int32 totalWidth, Char paddingChar)
at Microsoft.PowerShell.UpdatesNotification.ShowUpdateNotification(PSHostUserInterface hostUI)
at Microsoft.PowerShell.ConsoleHost.Start(String bannerText, String helpText, Boolean issProvidedExternally)

[已退出进程,代码为 2148734499 (0x80131623)]

特征:

  • 每次启动都崩,根本进不了交互界面
  • -NoProfile 无效
  • 换个窗口宽度、最大化、改字体都无效
  • 用脚本方式调用(输出重定向、非交互)反而不崩

二、先排除 Windows 更新

一开始最容易怀疑的是 Windows Update 动了 PowerShell,但可以很快排除:

检查项 结果
C:\Program Files\PowerShell\7\ 文件日期 9 月 2 日(构建时间),未变
注册表 HKLM\SOFTWARE\Microsoft\PowerShellCore\InstalledVersions 7.6.6
Windows Update 是否分发 PowerShell 7 否,PowerShell 7 是独立 MSI 安装

也就是说,安装本身没被动过。问题出在”运行时的某个条件”上。

三、定位:崩溃发生在更新提醒的格式化代码里

调用栈已经点名了函数:

1
Microsoft.PowerShell.UpdatesNotification.ShowUpdateNotification

对应源码(v7.6.6):

src/Microsoft.PowerShell.ConsoleHost/host/msh/UpdatesNotification.cs

关键片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// The first line is longest, if the message changes, this needs to be updated
int line1Length = notificationMsgTemplate.IndexOf('\n');
int line2Length = notificationMsgTemplate.IndexOf('\n', line1Length + 1);
int line3Length = notificationMsgTemplate.IndexOf('\n', line2Length + 1);

line3Length -= line2Length + 1;
line2Length -= line1Length + 1;

line2Padding = line2Padding.PadRight(
line1Length - line2Length + releaseTag.Length
);

// 3 represents the extra placeholder in the template
line3Padding = line3Padding.PadRight(
line1Length - line3Length + 3
);

注释写得很清楚:**”第一行是最长的”**。这段代码靠这个假设来计算后两行要补多少空格,让三行对齐。

问题是,这个假设在简体中文资源里不成立。

zh-Hans 的稳定版提醒文案:

1
2
3
{1} 有新的 PowerShell 稳定版本可用: v{0} {2}
{1} 立即升级,或访问发布页:{3}{2}
{1} https://aka.ms/PowerShell-Release?tag=v{0} {4}{2}

三行长度:

行 内容 长度
第 1 行 {1} 有新的 PowerShell 稳定版本可用: v{0} {2} 37
第 2 行 {1} 立即升级,或访问发布页:{3}{2} 24
第 3 行 {1} https://aka.ms/PowerShell-Release?tag=v{0} {4}{2} 57

代进代码:

1
2
3
line1Length - line3Length + 3
= 37 - 57 + 3
= -17

于是执行了等价于 "".PadRight(-17) 的操作。String.PadRight 不接受负数,抛出 ArgumentOutOfRangeException。由于这发生在控制台宿主启动阶段、没有上层可以接住,进程直接 FailFast 退出。

预览版和 LTS 文案同样有问题:

提醒类型 第 1 行长度 第 3 行长度 计算结果
稳定版 37 57 -17
预览版 37 57 -17
LTS 39 57 -15

所以这不是某个版本独有的,而是中文翻译文案与代码假设共同导致的。

四、为什么是”今天”突然开始崩

更新提醒不是发现新版本立刻弹,而是延迟 7 天。源码里写死了:

1
2
private const int NotificationDelayDays = 7;
private const int UpdateCheckBackoffDays = 7;

延迟的理由也写在注释里:新版本刚发布时镜像可能还在滚动,等一周再提醒,降低”看到了却装不上”的概率。

时间线完全对得上:

时间 事件
2026-09-08 PowerShell 7.6.6 发布(安装文件构建于 9 月 2 日)
2026-10-13 PowerShell 7.6.7 发布
2026-10-14 22:52 本机更新检查发现 7.6.7,写入缓存文件
2026-10-20 距 10-13 满 7 天,提醒开始显示
2026-10-20 早上 打开 PowerShell,首次触发提醒,启动即崩

本机缓存文件可以直接看到:

1
%LOCALAPPDATA%\Microsoft\PowerShell\7.6.6\update1_v7.6.7_2026-10-13

文件名里的日期就是新版本的发布日期,ShowUpdateNotification 正是拿它和当前时间比较来决定是否显示。

这也解释了两个现象:

  1. 为什么之前一直好好的:提醒还没到期。
  2. 为什么脚本里复现不了:崩溃只在 hostUI.SupportsVirtualTerminal 为真时才走补空格那段代码。真实交互式终端支持 VT,重定向/非交互环境通常不支持,于是绕过了崩溃点。

五、修复

5.1 关闭更新检查(推荐,立竿见影)

设置用户级环境变量:

1
[Environment]::SetEnvironmentVariable('POWERSHELL_UPDATECHECK', 'Off', 'User')

或者用传统方式:

1
setx POWERSHELL_UPDATECHECK Off

这个变量会让 UpdatesNotification.CanNotifyUpdates 变成 false。而 ConsoleHost.Start 只在它为真时才调用显示逻辑:

1
2
3
4
if (UpdatesNotification.CanNotifyUpdates)
{
UpdatesNotification.ShowUpdateNotification(hostUI);
}

所以提醒路径根本不会执行,也就不会崩。

5.2 验证是否生效

不设变量时(默认):

1
2
3
4
5
6
$t = [AppDomain]::CurrentDomain.GetAssemblies() |
ForEach-Object { $_.GetType('Microsoft.PowerShell.UpdatesNotification') } |
Where-Object { $_ }

$t.GetField('CanNotifyUpdates', 'NonPublic,Static').GetValue($null)
# True

设置 POWERSHELL_UPDATECHECK=Off 后:

1
# False

本机实测:不设变量为 True,设为 Off 后为 False。

5.3 让变量生效

环境变量只对新启动的进程生效,而已打开的终端会缓存旧环境:

  1. 关闭所有 PowerShell 窗口
  2. 如果用的是 Windows Terminal,退出整个应用(不是只关标签页)
  3. 重新打开 PowerShell 7

如果还不行,注销再登录,或重启资源管理器;最稳妥的是重启系统。

想在当前会话里立刻验证,可以:

1
2
$env:POWERSHELL_UPDATECHECK = 'Off'
pwsh

子进程会继承这个变量。

5.4 代价

  • 以后不会再收到 PowerShell 更新提醒,需要自己偶尔看一眼 Releases。
  • 这是临时规避,不是根治。等上游修复并发布新版本后,可以升级,再把变量改回 Default 或删除。

恢复提醒:

1
[Environment]::SetEnvironmentVariable('POWERSHELL_UPDATECHECK', $null, 'User')

5.5 为什么升级到 7.6.7 没用

我核对了 v7.6.7 的源码和 zh-Hans 资源文件:

  • UpdatesNotification.cs 里 PadRight(line1Length - line3Length + 3) 的逻辑原样保留
  • ManagedEntranceStrings.zh-Hans.resx 里三行文案的长度仍然是 37 / 24 / 57

也就是说,升级只是把”下一次崩溃”推迟到 7.6.8 发布后的第 7 天。必须等官方修复这个格式化逻辑。

5.6 其他方案为什么不行

方案 结果
把窗口拉宽 / 最大化 无效。-17 来自字符串长度,和窗口宽度无关
pwsh -NoProfile 无效。崩溃发生在加载 profile 之前
切到 LTS 更新通道 无效。LTS 的 zh-Hans 文案同样算出负数(-15)
删除 update1_* 缓存文件 无效。下次启动会重新检查并写回,提醒条件依然成立
把系统语言改成英文 能绕过,但代价过大,不推荐
修改安装目录里的资源 DLL 不推荐。MSI 修复/升级会覆盖,且破坏签名完整性

六、上游状态

  • Issue:PowerShell/PowerShell#28131
  • 标题:PowerShell can crash on startup when displaying the update notification under zh-Hans due to negative PadRight width
  • 状态:open,Needs-Triage
  • 报告者在 7.7.0-preview.4 上首次遇到,并确认 7.7.0-preview.5 仍有同样代码和文案
  • 2026-10-20 仍有用户回复 “same”,说明影响面在扩大

修复思路本身不复杂:取三行中的最大长度,或者对负值做兜底(Math.Max(0, ...))。但在官方发版前,本地只能先关掉提醒。

七、可复用的排查经验

  1. 先看调用栈,别猜。ShowUpdateNotification 已经把范围缩到一个函数,比盲目重装快得多。
  2. **区分”安装被改了”和”运行时条件触发了”**。文件日期 + 注册表版本就能排除 Windows Update。
  3. 注意时间相关的功能。更新提醒有”延迟 N 天”的设计,这类 bug 往往在发布后第 N 天才出现,看起来像是”今天突然坏了”。
  4. 负宽度、负数索引这类异常,优先怀疑本地化文案。代码按英文文案的长度关系写死假设,翻译一变就翻车。
  5. 交互式才崩、脚本不崩,说明和终端能力有关。本例是 SupportsVirtualTerminal 分支。

八、参考