用户目录在ReFS文件系统导致更新失败
这台机器上,凡是需要重启的更新,装完重启就回滚,错误码还每次不一样:0x80071AB0、0x80070020、0x80028017 都见过。系统版本一直停在装机时的 26200.9168,之后操作系统累积更新一个都没打上。
原因在用户目录:它被放在了 D:\Users,而 D: 是 ReFS 卷。累积更新在重启阶段要用内核事务(KTM/TxF)删掉一条默认用户配置文件里的 Desktop.ini,ReFS 不支持事务化的写和删,这条原语必然失败,整条队列回滚。
机器情况:Windows 11 25H2,26200.9168;D: 约 1 TB,ReFS,用户目录由装机时的 unattend.xml 指定;反复失败的更新是 KB5124010(26200.9550)。
日志里的那一行
C:\Windows\Logs\CBS\CBS.log:
Error CSI 00000001 (F) c019003f [Error,Facility=FACILITY_TRANSACTION,Code=63 (0x003f)]
from ...DirectFileSystemProvider::SysCreateFile(
da = (DELETE|SYNCHRONIZE|FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES|FILE_READ_DATA),
on:[98]'\??\D:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Accessories\Desktop.ini'
Failure in poqexec.exe while processing updates. [HRESULT = 0x80071ab0 - ERROR_TRANSACTIONAL_OPEN_NOT_ALLOWED]
Startup: Failed while processing critical primitive operations queue.
同一条日志里,WER 报告把失败的操作写得更直白:
WER: Generating failure report for package: Package_for_RollupFix~31bf3856ad364e35~amd64~~26100.9550.1.28,
status: 0x80071ab0, failure source: POQ, start state: Staged, target state: Installed
failure details: "POQ Operation DeleteFile OperationData \??\D:\Users\Default\...\Accessories\Desktop.ini"
C:\Windows\WinSxS\poqexec.log 会跨日志轮转保留历史。同一条操作反复出现:
a42, c019003f, 3d65, 0, DeleteFile ;\??\D:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Accessories\Desktop.ini
把每段开头的十六进制时间戳解出来:23 次重启期服务化,17 次失败,全部卡在这一条,最早一次是 2026-09-02。
用户目录为什么在 D:
C:\Windows\Panther\unattend.xml 是装机时留下的:
<component name="Microsoft-Windows-Shell-Setup" ...>
<FolderLocations>
<ProfilesDirectory>D:\Users</ProfilesDirectory>
</FolderLocations>
</component>
注册表里确认已经生效:
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList').ProfilesDirectory
# D:\Users
Get-Volume -DriveLetter D | Select-Object FileSystemType
# ReFS
累积更新会为「默认用户配置文件」生成一组 POQ 原语(删除 …\Default\…\Desktop.ini 这类文件),路径取自 ProfilesDirectory。所以每条这类原语都落在 ReFS 卷上。
复现
poqexec 执行原语用的是 CreateFileTransacted / DeleteFileTransacted。用同一组 API 在几个卷上各测一遍,结果分得很干净:
| 卷 | 文件系统 | 事务化只读打开 | 事务化写入/删除打开 |
|---|---|---|---|
D: |
ReFS | 通过 | 失败 0x1AB0 |
E: |
NTFS | 通过 | 通过 |
失败的那个码和更新失败时完全一致:0x1AB0 ERROR_TRANSACTIONAL_OPEN_NOT_ALLOWED(「不允许在事务中打开该对象」)。实测下来,ReFS 允许只读的事务化打开,凡带写或删除意图的事务化打开一律被拒,而 poqexec 要的正是带 DELETE 的打开。
复现脚本
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class TxF {
[DllImport("ktmw32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern IntPtr CreateTransaction(IntPtr sa, IntPtr guid, uint options, uint timeout, uint descLen, string desc);
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern IntPtr CreateFileTransactedW(string name, uint access, uint share, IntPtr sa, uint disp, uint flags, IntPtr tmpl, IntPtr tx, IntPtr mini, IntPtr ext);
[DllImport("ktmw32.dll", SetLastError=true)] public static extern bool RollbackTransaction(IntPtr tx);
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool CloseHandle(IntPtr h);
}
'@
function Test-TxOpen([string]$path, [uint32]$access, [string]$label) {
New-Item -Path $path -ItemType File -Force | Out-Null
$tx = [TxF]::CreateTransaction([IntPtr]::Zero, [IntPtr]::Zero, [uint32]0, [uint32]0, [uint32]0, $null)
$h = [TxF]::CreateFileTransactedW($path, $access, [uint32]7, [IntPtr]::Zero, [uint32]3, [uint32]128,
[IntPtr]::Zero, $tx, [IntPtr]::Zero, [IntPtr]::Zero)
if ($h.ToInt64() -eq -1) {
$e = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
"{0} -> 0x{1:X8} {2}" -f $label, $e, [ComponentModel.Win32Exception]::new($e).Message
} else {
"{0} -> OK" -f $label
[void][TxF]::CloseHandle($h)
}
[void][TxF]::RollbackTransaction($tx); [void][TxF]::CloseHandle($tx)
Remove-Item $path -Force
}
$READ = [uint32]2147483648; $DELETE = [uint32]65536
Test-TxOpen 'D:\txf_probe.txt' $READ 'D: 只读'
Test-TxOpen 'D:\txf_probe.txt' $DELETE 'D: 删除'
Test-TxOpen 'E:\txf_probe.txt' $READ 'E: 只读'
Test-TxOpen 'E:\txf_probe.txt' $DELETE 'E: 删除'
有件事得说清楚:这份测试在 C: 上、以普通用户令牌运行时,连只读的事务化打开都会被拒;而服务化引擎以 SYSTEM 身份运行,在 C: 上一直有成功提交事务的记录。所以那一行属于非提权测试自身的局限,不是阻塞点,也不影响 D: 的结论。
为什么错误码每次都不同
POQ 队列是整体提交的。任何一条原语失败,整条队列中止、更新回滚,而失败发生在关机阶段还是启动阶段、最终在哪一层被包装,报出来的码就不一样:
- 关机阶段:
0x80070020 ERROR_SHARING_VIOLATION - 启动阶段:
0x80071ab0 ERROR_TRANSACTIONAL_OPEN_NOT_ALLOWED - Windows 更新界面:
0x80028017
反过来也成立:不含这条原语的包能装上。9 月 27 日的 KB5126052 是 .NET Framework 4.8.1 更新,同样需要重启,也走同一条 POQ 路径,就成功了。卡住的只有操作系统累积更新。
修复
ReFS 不能就地转成 NTFS,只能二选一:
- 把
D:重新格式化为 NTFS(推荐)。robocopy /COPYALL /MIR /XJ备份D:\Users等目录(这台机器上约 190 GB),删除并重建D:分区为 NTFS,数据回迁时注意Default、Public的隐藏和系统属性。注册表和 unattend 都不用动。 - 把用户目录迁到 NTFS 卷。迁移数据后改
ProfileList里的ProfilesDirectory和各 SID 的ProfileImagePath。微软只在装机时支持设置这个值,装机后改属于非官方路径,有配置文件损坏风险,建议配合新建用户做。
Dev Drive 也是 ReFS,适合放源码和包缓存,不要拿来放用户目录。
验证
更新并重启之后:
# 最后一次应当是 CommitTransaction,不该再出现 c019003f
Get-Content C:\Windows\WinSxS\poqexec.log -Tail 20
# 期望 [HRESULT = 0x0]
Select-String C:\Windows\Logs\CBS\CBS.log -Pattern 'Startup processing completed'